Merge pull request #336 from nettanvirdev/main
Update: Updated The AgenticSeek Frontend
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,329 +1,418 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from "react-markdown";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import { colors } from './colors';
|
import { ThemeToggle } from "./components/ThemeToggle";
|
||||||
|
import { ResizableLayout } from "./components/ResizableLayout";
|
||||||
|
import faviconPng from "./logo.png";
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:7777';
|
||||||
|
console.log("Using backend URL:", BACKEND_URL);
|
||||||
|
|
||||||
const BACKEND_URL = 'http://localhost:7777' || process.env.REACT_APP_BACKEND_URL;
|
|
||||||
console.log('Using backend URL:', BACKEND_URL);
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState("");
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [currentView, setCurrentView] = useState('blocks');
|
const [currentView, setCurrentView] = useState("blocks");
|
||||||
const [responseData, setResponseData] = useState(null);
|
const [responseData, setResponseData] = useState(null);
|
||||||
const [isOnline, setIsOnline] = useState(false);
|
const [isOnline, setIsOnline] = useState(false);
|
||||||
const [status, setStatus] = useState('Agents ready');
|
const [status, setStatus] = useState("Agents ready");
|
||||||
const [expandedReasoning, setExpandedReasoning] = useState(new Set());
|
const [expandedReasoning, setExpandedReasoning] = useState(new Set());
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchLatestAnswer = useCallback(async () => {
|
||||||
const intervalId = setInterval(() => {
|
try {
|
||||||
checkHealth();
|
const res = await axios.get(`${BACKEND_URL}/latest_answer`);
|
||||||
fetchLatestAnswer();
|
const data = res.data;
|
||||||
fetchScreenshot();
|
|
||||||
}, 3000);
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
const checkHealth = async () => {
|
updateData(data);
|
||||||
try {
|
if (!data.answer || data.answer.trim() === "") {
|
||||||
await axios.get(`${BACKEND_URL}/health`);
|
return;
|
||||||
setIsOnline(true);
|
}
|
||||||
console.log('System is online');
|
const normalizedNewAnswer = normalizeAnswer(data.answer);
|
||||||
} catch {
|
const answerExists = messages.some(
|
||||||
setIsOnline(false);
|
(msg) => normalizeAnswer(msg.content) === normalizedNewAnswer
|
||||||
console.log('System is offline');
|
);
|
||||||
}
|
if (!answerExists) {
|
||||||
};
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
const fetchScreenshot = async () => {
|
{
|
||||||
try {
|
type: "agent",
|
||||||
const timestamp = new Date().getTime();
|
content: data.answer,
|
||||||
const res = await axios.get(`${BACKEND_URL}/screenshots/updated_screen.png?timestamp=${timestamp}`, {
|
reasoning: data.reasoning,
|
||||||
responseType: 'blob'
|
agentName: data.agent_name,
|
||||||
});
|
|
||||||
console.log('Screenshot fetched successfully');
|
|
||||||
const imageUrl = URL.createObjectURL(res.data);
|
|
||||||
setResponseData((prev) => {
|
|
||||||
if (prev?.screenshot && prev.screenshot !== 'placeholder.png') {
|
|
||||||
URL.revokeObjectURL(prev.screenshot);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
screenshot: imageUrl,
|
|
||||||
screenshotTimestamp: new Date().getTime()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching screenshot:', err);
|
|
||||||
setResponseData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
screenshot: 'placeholder.png',
|
|
||||||
screenshotTimestamp: new Date().getTime()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAnswer = (answer) => {
|
|
||||||
return answer
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.replace(/[.,!?]/g, '')
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleReasoning = (messageIndex) => {
|
|
||||||
setExpandedReasoning(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
if (newSet.has(messageIndex)) {
|
|
||||||
newSet.delete(messageIndex);
|
|
||||||
} else {
|
|
||||||
newSet.add(messageIndex);
|
|
||||||
}
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLatestAnswer = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(`${BACKEND_URL}/latest_answer`);
|
|
||||||
const data = res.data;
|
|
||||||
|
|
||||||
updateData(data);
|
|
||||||
if (!data.answer || data.answer.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const normalizedNewAnswer = normalizeAnswer(data.answer);
|
|
||||||
const answerExists = messages.some(
|
|
||||||
(msg) => normalizeAnswer(msg.content) === normalizedNewAnswer
|
|
||||||
);
|
|
||||||
if (!answerExists) {
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: 'agent',
|
|
||||||
content: data.answer,
|
|
||||||
reasoning: data.reasoning,
|
|
||||||
agentName: data.agent_name,
|
|
||||||
status: data.status,
|
|
||||||
uid: data.uid,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
setStatus(data.status);
|
|
||||||
scrollToBottom();
|
|
||||||
} else {
|
|
||||||
console.log('Duplicate answer detected, skipping:', data.answer);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching latest answer:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateData = (data) => {
|
|
||||||
setResponseData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
blocks: data.blocks || prev.blocks || null,
|
|
||||||
done: data.done,
|
|
||||||
answer: data.answer,
|
|
||||||
agent_name: data.agent_name,
|
|
||||||
status: data.status,
|
status: data.status,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
}));
|
},
|
||||||
};
|
]);
|
||||||
|
setStatus(data.status);
|
||||||
const handleStop = async (e) => {
|
scrollToBottom();
|
||||||
e.preventDefault();
|
} else {
|
||||||
checkHealth();
|
console.log("Duplicate answer detected, skipping:", data.answer);
|
||||||
setIsLoading(false);
|
}
|
||||||
setError(null);
|
} catch (error) {
|
||||||
try {
|
console.error("Error fetching latest answer:", error);
|
||||||
const res = await axios.get(`${BACKEND_URL}/stop`);
|
|
||||||
setStatus("Requesting stop...");
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error stopping the agent:', err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
useEffect(() => {
|
||||||
e.preventDefault();
|
const intervalId = setInterval(() => {
|
||||||
checkHealth();
|
checkHealth();
|
||||||
if (!query.trim()) {
|
fetchLatestAnswer();
|
||||||
console.log('Empty query');
|
fetchScreenshot();
|
||||||
return;
|
}, 3000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [fetchLatestAnswer]);
|
||||||
|
|
||||||
|
const checkHealth = async () => {
|
||||||
|
try {
|
||||||
|
await axios.get(`${BACKEND_URL}/health`);
|
||||||
|
setIsOnline(true);
|
||||||
|
console.log("System is online");
|
||||||
|
} catch {
|
||||||
|
setIsOnline(false);
|
||||||
|
console.log("System is offline");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchScreenshot = async () => {
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
const res = await axios.get(
|
||||||
|
`${BACKEND_URL}/screenshots/updated_screen.png?timestamp=${timestamp}`,
|
||||||
|
{
|
||||||
|
responseType: "blob",
|
||||||
}
|
}
|
||||||
setMessages((prev) => [...prev, { type: 'user', content: query }]);
|
);
|
||||||
setIsLoading(true);
|
console.log("Screenshot fetched successfully");
|
||||||
setError(null);
|
const imageUrl = URL.createObjectURL(res.data);
|
||||||
|
setResponseData((prev) => {
|
||||||
try {
|
if (prev?.screenshot && prev.screenshot !== "placeholder.png") {
|
||||||
console.log('Sending query:', query);
|
URL.revokeObjectURL(prev.screenshot);
|
||||||
setQuery('waiting for response...');
|
|
||||||
const res = await axios.post(`${BACKEND_URL}/query`, {
|
|
||||||
query,
|
|
||||||
tts_enabled: false
|
|
||||||
});
|
|
||||||
setQuery('Enter your query...');
|
|
||||||
console.log('Response:', res.data);
|
|
||||||
const data = res.data;
|
|
||||||
updateData(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error:', err);
|
|
||||||
setError('Failed to process query.');
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ type: 'error', content: 'Error: Unable to get a response.' },
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
console.log('Query completed');
|
|
||||||
setIsLoading(false);
|
|
||||||
setQuery('');
|
|
||||||
}
|
}
|
||||||
};
|
return {
|
||||||
|
...prev,
|
||||||
|
screenshot: imageUrl,
|
||||||
|
screenshotTimestamp: new Date().getTime(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching screenshot:", err);
|
||||||
|
setResponseData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
screenshot: "placeholder.png",
|
||||||
|
screenshotTimestamp: new Date().getTime(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGetScreenshot = async () => {
|
const normalizeAnswer = (answer) => {
|
||||||
try {
|
return answer
|
||||||
setCurrentView('screenshot');
|
.trim()
|
||||||
} catch (err) {
|
.toLowerCase()
|
||||||
setError('Browser not in use');
|
.replace(/\s+/g, " ")
|
||||||
}
|
.replace(/[.,!?]/g, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const scrollToBottom = () => {
|
||||||
<div className="app">
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
<header className="header">
|
};
|
||||||
<h1>AgenticSeek</h1>
|
|
||||||
</header>
|
|
||||||
<main className="main">
|
|
||||||
<div className="app-sections">
|
|
||||||
<div className="chat-section">
|
|
||||||
<h2>Chat Interface</h2>
|
|
||||||
<div className="messages">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<p className="placeholder">No messages yet. Type below to start!</p>
|
|
||||||
) : (
|
|
||||||
messages.map((msg, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`message ${
|
|
||||||
msg.type === 'user'
|
|
||||||
? 'user-message'
|
|
||||||
: msg.type === 'agent'
|
|
||||||
? 'agent-message'
|
|
||||||
: 'error-message'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="message-header">
|
|
||||||
{msg.type === 'agent' && (
|
|
||||||
<span className="agent-name">{msg.agentName}</span>
|
|
||||||
)}
|
|
||||||
{msg.type === 'agent' && msg.reasoning && expandedReasoning.has(index) && (
|
|
||||||
<div className="reasoning-content">
|
|
||||||
<ReactMarkdown>{msg.reasoning}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{msg.type === 'agent' && (
|
|
||||||
<button
|
|
||||||
className="reasoning-toggle"
|
|
||||||
onClick={() => toggleReasoning(index)}
|
|
||||||
title={expandedReasoning.has(index) ? "Hide reasoning" : "Show reasoning"}
|
|
||||||
>
|
|
||||||
{expandedReasoning.has(index) ? '▼' : '▶'} Reasoning
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="message-content">
|
|
||||||
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
{isOnline && <div className="loading-animation">{status}</div>}
|
|
||||||
{!isLoading && !isOnline && <p className="loading-animation">System offline. Deploy backend first.</p>}
|
|
||||||
<form onSubmit={handleSubmit} className="input-form">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder="Type your query..."
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<button type="submit" disabled={isLoading}>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
<button onClick={handleStop}>
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="computer-section">
|
const toggleReasoning = (messageIndex) => {
|
||||||
<h2>Computer View</h2>
|
setExpandedReasoning((prev) => {
|
||||||
<div className="view-selector">
|
const newSet = new Set(prev);
|
||||||
<button
|
if (newSet.has(messageIndex)) {
|
||||||
className={currentView === 'blocks' ? 'active' : ''}
|
newSet.delete(messageIndex);
|
||||||
onClick={() => setCurrentView('blocks')}
|
} else {
|
||||||
>
|
newSet.add(messageIndex);
|
||||||
Editor View
|
}
|
||||||
</button>
|
return newSet;
|
||||||
<button
|
});
|
||||||
className={currentView === 'screenshot' ? 'active' : ''}
|
};
|
||||||
onClick={responseData?.screenshot ? () => setCurrentView('screenshot') : handleGetScreenshot}
|
|
||||||
>
|
const updateData = (data) => {
|
||||||
Browser View
|
setResponseData((prev) => ({
|
||||||
</button>
|
...prev,
|
||||||
</div>
|
blocks: data.blocks || prev.blocks || null,
|
||||||
<div className="content">
|
done: data.done,
|
||||||
{error && <p className="error">{error}</p>}
|
answer: data.answer,
|
||||||
{currentView === 'blocks' ? (
|
agent_name: data.agent_name,
|
||||||
<div className="blocks">
|
status: data.status,
|
||||||
{responseData && responseData.blocks && Object.values(responseData.blocks).length > 0 ? (
|
uid: data.uid,
|
||||||
Object.values(responseData.blocks).map((block, index) => (
|
}));
|
||||||
<div key={index} className="block">
|
};
|
||||||
<p className="block-tool">Tool: {block.tool_type}</p>
|
|
||||||
<pre>{block.block}</pre>
|
const handleStop = async (e) => {
|
||||||
<p className="block-feedback">Feedback: {block.feedback}</p>
|
e.preventDefault();
|
||||||
{block.success ? (
|
checkHealth();
|
||||||
<p className="block-success">Success</p>
|
setIsLoading(false);
|
||||||
) : (
|
setError(null);
|
||||||
<p className="block-failure">Failure</p>
|
try {
|
||||||
)}
|
await axios.get(`${BACKEND_URL}/stop`);
|
||||||
</div>
|
setStatus("Requesting stop...");
|
||||||
))
|
} catch (err) {
|
||||||
) : (
|
console.error("Error stopping the agent:", err);
|
||||||
<div className="block">
|
}
|
||||||
<p className="block-tool">Tool: No tool in use</p>
|
};
|
||||||
<pre>No file opened</pre>
|
|
||||||
</div>
|
const handleSubmit = async (e) => {
|
||||||
)}
|
e.preventDefault();
|
||||||
</div>
|
checkHealth();
|
||||||
) : (
|
if (!query.trim()) {
|
||||||
<div className="screenshot">
|
console.log("Empty query");
|
||||||
<img
|
return;
|
||||||
src={responseData?.screenshot || 'placeholder.png'}
|
}
|
||||||
alt="Screenshot"
|
setMessages((prev) => [...prev, { type: "user", content: query }]);
|
||||||
onError={(e) => {
|
setIsLoading(true);
|
||||||
e.target.src = 'placeholder.png';
|
setError(null);
|
||||||
console.error('Failed to load screenshot');
|
|
||||||
}}
|
try {
|
||||||
key={responseData?.screenshotTimestamp || 'default'}
|
console.log("Sending query:", query);
|
||||||
/>
|
setQuery("waiting for response...");
|
||||||
</div>
|
const res = await axios.post(`${BACKEND_URL}/query`, {
|
||||||
)}
|
query,
|
||||||
</div>
|
tts_enabled: false,
|
||||||
</div>
|
});
|
||||||
</div>
|
setQuery("Enter your query...");
|
||||||
</main>
|
console.log("Response:", res.data);
|
||||||
|
const data = res.data;
|
||||||
|
updateData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error:", err);
|
||||||
|
setError("Failed to process query.");
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ type: "error", content: "Error: Unable to get a response." },
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
console.log("Query completed");
|
||||||
|
setIsLoading(false);
|
||||||
|
setQuery("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetScreenshot = async () => {
|
||||||
|
try {
|
||||||
|
setCurrentView("screenshot");
|
||||||
|
} catch (err) {
|
||||||
|
setError("Browser not in use");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="header">
|
||||||
|
<div className="header-brand">
|
||||||
|
<div className="logo-container">
|
||||||
|
<img src={faviconPng} alt="AgenticSeek" className="logo-icon" />
|
||||||
|
</div>
|
||||||
|
<div className="brand-text">
|
||||||
|
<h1>AgenticSeek</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="header-status">
|
||||||
|
<div
|
||||||
|
className={`status-indicator ${isOnline ? "online" : "offline"}`}
|
||||||
|
>
|
||||||
|
<div className="status-dot"></div>
|
||||||
|
<span className="status-text">
|
||||||
|
{isOnline ? "Online" : "Offline"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="header-actions">
|
||||||
|
<a
|
||||||
|
href="https://github.com/Fosowl/agenticSeek"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="action-button github-link"
|
||||||
|
aria-label="View on GitHub"
|
||||||
|
>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
<span className="action-text">GitHub</span>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="main">
|
||||||
|
<ResizableLayout initialLeftWidth={50}>
|
||||||
|
<div className="chat-section">
|
||||||
|
<h2>Chat Interface</h2>
|
||||||
|
<div className="messages">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<p className="placeholder">
|
||||||
|
No messages yet. Type below to start!
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
messages.map((msg, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`message ${
|
||||||
|
msg.type === "user"
|
||||||
|
? "user-message"
|
||||||
|
: msg.type === "agent"
|
||||||
|
? "agent-message"
|
||||||
|
: "error-message"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="message-header">
|
||||||
|
{msg.type === "agent" && (
|
||||||
|
<span className="agent-name">{msg.agentName}</span>
|
||||||
|
)}
|
||||||
|
{msg.type === "agent" &&
|
||||||
|
msg.reasoning &&
|
||||||
|
expandedReasoning.has(index) && (
|
||||||
|
<div className="reasoning-content">
|
||||||
|
<ReactMarkdown>{msg.reasoning}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{msg.type === "agent" && (
|
||||||
|
<button
|
||||||
|
className="reasoning-toggle"
|
||||||
|
onClick={() => toggleReasoning(index)}
|
||||||
|
title={
|
||||||
|
expandedReasoning.has(index)
|
||||||
|
? "Hide reasoning"
|
||||||
|
: "Show reasoning"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{expandedReasoning.has(index) ? "▼" : "▶"} Reasoning
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
<ReactMarkdown>{msg.content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
{isOnline && <div className="loading-animation">{status}</div>}
|
||||||
|
{!isLoading && !isOnline && (
|
||||||
|
<p className="loading-animation">
|
||||||
|
System offline. Deploy backend first.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit} className="input-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Type your query..."
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<div className="action-buttons">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="icon-button"
|
||||||
|
aria-label="Send message"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path
|
||||||
|
d="M22 2L11 13M22 2L15 22L11 13M22 2L2 9L11 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStop}
|
||||||
|
className="icon-button stop-button"
|
||||||
|
aria-label="Stop processing"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect
|
||||||
|
x="6"
|
||||||
|
y="6"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
fill="currentColor"
|
||||||
|
rx="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="computer-section">
|
||||||
|
<h2>Computer View</h2>
|
||||||
|
<div className="view-selector">
|
||||||
|
<button
|
||||||
|
className={currentView === "blocks" ? "active" : ""}
|
||||||
|
onClick={() => setCurrentView("blocks")}
|
||||||
|
>
|
||||||
|
Editor View
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={currentView === "screenshot" ? "active" : ""}
|
||||||
|
onClick={
|
||||||
|
responseData?.screenshot
|
||||||
|
? () => setCurrentView("screenshot")
|
||||||
|
: handleGetScreenshot
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Browser View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
{currentView === "blocks" ? (
|
||||||
|
<div className="blocks">
|
||||||
|
{responseData &&
|
||||||
|
responseData.blocks &&
|
||||||
|
Object.values(responseData.blocks).length > 0 ? (
|
||||||
|
Object.values(responseData.blocks).map((block, index) => (
|
||||||
|
<div key={index} className="block">
|
||||||
|
<p className="block-tool">Tool: {block.tool_type}</p>
|
||||||
|
<pre>{block.block}</pre>
|
||||||
|
<p className="block-feedback">
|
||||||
|
Feedback: {block.feedback}
|
||||||
|
</p>
|
||||||
|
{block.success ? (
|
||||||
|
<p className="block-success">Success</p>
|
||||||
|
) : (
|
||||||
|
<p className="block-failure">Failure</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="block">
|
||||||
|
<p className="block-tool">Tool: No tool in use</p>
|
||||||
|
<pre>No file opened</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="screenshot">
|
||||||
|
<img
|
||||||
|
src={responseData?.screenshot || "placeholder.png"}
|
||||||
|
alt="Screenshot"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.src = "placeholder.png";
|
||||||
|
console.error("Failed to load screenshot");
|
||||||
|
}}
|
||||||
|
key={responseData?.screenshotTimestamp || "default"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizableLayout>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,63 +1,64 @@
|
|||||||
export const colors = {
|
export const colors = {
|
||||||
// Primary colors
|
// Primary colors - matching the dashboard theme
|
||||||
primary: '#0066cc',
|
primary: "#2563eb",
|
||||||
primaryLight: '#e6f2ff',
|
primaryLight: "#dbeafe",
|
||||||
primaryDark: '#004c99',
|
primaryDark: "#1d4ed8",
|
||||||
|
|
||||||
// Secondary colors
|
// Secondary colors - modern grays
|
||||||
secondary: '#6c757d',
|
secondary: "#64748b",
|
||||||
secondaryLight: '#f8f9fa',
|
secondaryLight: "#f1f5f9",
|
||||||
secondaryDark: '#343a40',
|
secondaryDark: "#1e293b",
|
||||||
|
|
||||||
// Accent colors
|
// Accent colors
|
||||||
accent: '#ff9500',
|
accent: "#f59e0b",
|
||||||
accentLight: '#fff4e6',
|
accentLight: "#fef3c7",
|
||||||
accentDark: '#cc7a00',
|
accentDark: "#d97706",
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
success: '#28a745',
|
success: "#10b981",
|
||||||
successLight: '#e8f5e9',
|
successLight: "#d1fae5",
|
||||||
warning: '#ffc107',
|
warning: "#f59e0b",
|
||||||
warningLight: '#fff9e6',
|
warningLight: "#fef3c7",
|
||||||
error: '#dc3545',
|
error: "#ef4444",
|
||||||
errorLight: '#ffebee',
|
errorLight: "#fee2e2",
|
||||||
info: '#17a2b8',
|
info: "#06b6d4",
|
||||||
infoLight: '#e3f2fd',
|
infoLight: "#cffafe",
|
||||||
|
|
||||||
// Neutral colors
|
// Neutral colors - modern palette
|
||||||
white: '#ffffff',
|
white: "#ffffff",
|
||||||
gray100: '#f8f9fa',
|
gray50: "#f8fafc",
|
||||||
gray200: '#e9ecef',
|
gray100: "#f1f5f9",
|
||||||
gray300: '#dee2e6',
|
gray200: "#e2e8f0",
|
||||||
gray400: '#ced4da',
|
gray300: "#cbd5e1",
|
||||||
gray500: '#adb5bd',
|
gray400: "#94a3b8",
|
||||||
gray600: '#6c757d',
|
gray500: "#64748b",
|
||||||
gray700: '#495057',
|
gray600: "#475569",
|
||||||
gray800: '#343a40',
|
gray700: "#334155",
|
||||||
gray900: '#212529',
|
gray800: "#1e293b",
|
||||||
black: '#000000',
|
gray900: "#0f172a",
|
||||||
|
black: "#000000",
|
||||||
|
|
||||||
// Text colors
|
// Text colors
|
||||||
textPrimary: '#212529',
|
textPrimary: "#0f172a",
|
||||||
textSecondary: '#6c757d',
|
textSecondary: "#64748b",
|
||||||
textDisabled: '#adb5bd',
|
textDisabled: "#94a3b8",
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
background: '#f8f8f8',
|
background: "#f8fafc",
|
||||||
card: '#ffffff',
|
card: "#ffffff",
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
border: '#dee2e6',
|
border: "#e2e8f0",
|
||||||
divider: '#e9ecef',
|
divider: "#f1f5f9",
|
||||||
|
|
||||||
// Transparent colors
|
// Transparent colors
|
||||||
transparent: 'transparent',
|
transparent: "transparent",
|
||||||
semiTransparent: 'rgba(0, 0, 0, 0.5)',
|
semiTransparent: "rgba(15, 23, 42, 0.6)",
|
||||||
|
|
||||||
// Dark theme colors
|
// Dark theme colors
|
||||||
darkBackground: '#0f172a',
|
darkBackground: "#0f172a",
|
||||||
darkCard: '#1e293b',
|
darkCard: "#1e293b",
|
||||||
darkBorder: '#334155',
|
darkBorder: "#334155",
|
||||||
darkText: '#f8fafc',
|
darkText: "#f8fafc",
|
||||||
darkTextSecondary: '#cbd5e1',
|
darkTextSecondary: "#cbd5e1",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.resizable-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-left,
|
||||||
|
.resizable-right {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
width: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: col-resize;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 2px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: var(--border);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover .resize-handle-line {
|
||||||
|
background-color: var(--accent-foreground);
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-container.dragging .resize-handle {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-container.dragging .resize-handle-line {
|
||||||
|
background-color: var(--accent-foreground);
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.resizable-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-left,
|
||||||
|
.resizable-right {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState, useRef, useCallback } from "react";
|
||||||
|
import "./ResizableLayout.css";
|
||||||
|
|
||||||
|
export const ResizableLayout = ({ children, initialLeftWidth = 50 }) => {
|
||||||
|
const [leftWidth, setLeftWidth] = useState(initialLeftWidth);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (!isDragging || !containerRef.current) return;
|
||||||
|
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
const newLeftWidth =
|
||||||
|
((e.clientX - containerRect.left) / containerRect.width) * 100;
|
||||||
|
|
||||||
|
// Constrain between 20% and 80%
|
||||||
|
const constrainedWidth = Math.max(20, Math.min(80, newLeftWidth));
|
||||||
|
setLeftWidth(constrainedWidth);
|
||||||
|
},
|
||||||
|
[isDragging]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
} else {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
};
|
||||||
|
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`resizable-container ${isDragging ? "dragging" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="resizable-left" style={{ width: `${leftWidth}%` }}>
|
||||||
|
{children[0]}
|
||||||
|
</div>
|
||||||
|
<div className="resize-handle" onMouseDown={handleMouseDown}>
|
||||||
|
<div className="resize-handle-line" />
|
||||||
|
</div>
|
||||||
|
<div className="resizable-right" style={{ width: `${100 - leftWidth}%` }}>
|
||||||
|
{children[1]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
|
|
||||||
|
export const ThemeToggle = () => {
|
||||||
|
const { isDark, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="theme-toggle"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{isDark ? (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="5" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path
|
||||||
|
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path
|
||||||
|
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export const ThemeProvider = ({ children }) => {
|
||||||
|
const [isDark, setIsDark] = useState(() => {
|
||||||
|
const saved = localStorage.getItem("theme");
|
||||||
|
return saved ? saved === "dark" : true; // Default to dark
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
isDark ? "dark" : "light"
|
||||||
|
);
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
const toggleTheme = () => setIsDark(!isDark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ isDark, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from "react-dom/client";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
|
import "./styles/globals.css";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
|
After Width: | Height: | Size: 148 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,217 @@
|
|||||||
|
:root {
|
||||||
|
--background: hsl(0 0% 100%);
|
||||||
|
--foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--muted: hsl(210 40% 96.1%);
|
||||||
|
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||||
|
--popover: hsl(0 0% 100%);
|
||||||
|
--popover-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--card: hsl(0 0% 100%);
|
||||||
|
--card-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--border: hsl(214.3 31.8% 91.4%);
|
||||||
|
--input: hsl(214.3 31.8% 91.4%);
|
||||||
|
--primary: hsl(222.2 47.4% 11.2%);
|
||||||
|
--primary-foreground: hsl(210 40% 98%);
|
||||||
|
--secondary: hsl(210 40% 96.1%);
|
||||||
|
--secondary-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--accent: hsl(210 40% 96.1%);
|
||||||
|
--accent-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--destructive: hsl(0 100% 50%);
|
||||||
|
--destructive-foreground: hsl(210 40% 98%);
|
||||||
|
--ring: hsl(215 20.2% 65.1%);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: hsl(224 71% 4%);
|
||||||
|
--foreground: hsl(213 31% 91%);
|
||||||
|
--muted: hsl(223 47% 11%);
|
||||||
|
--muted-foreground: hsl(215.4 16.3% 56.9%);
|
||||||
|
--popover: hsl(224 71% 4%);
|
||||||
|
--popover-foreground: hsl(215 20.2% 65.1%);
|
||||||
|
--card: hsl(224 71% 4%);
|
||||||
|
--card-foreground: hsl(213 31% 91%);
|
||||||
|
--border: hsl(216 34% 17%);
|
||||||
|
--input: hsl(216 34% 17%);
|
||||||
|
--primary: hsl(210 40% 98%);
|
||||||
|
--primary-foreground: hsl(222.2 47.4% 1.2%);
|
||||||
|
--secondary: hsl(222.2 47.4% 11.2%);
|
||||||
|
--secondary-foreground: hsl(210 40% 98%);
|
||||||
|
--accent: hsl(216 34% 17%);
|
||||||
|
--accent-foreground: hsl(210 40% 98%);
|
||||||
|
--destructive: hsl(0 63% 31%);
|
||||||
|
--destructive-foreground: hsl(210 40% 98%);
|
||||||
|
--ring: hsl(216 34% 17%);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #fafafa;
|
||||||
|
--card: #1a1a1a;
|
||||||
|
--card-foreground: #fafafa;
|
||||||
|
--popover: #1a1a1a;
|
||||||
|
--popover-foreground: #fafafa;
|
||||||
|
--primary: #fafafa;
|
||||||
|
--primary-foreground: #0a0a0a;
|
||||||
|
--secondary: #2a2a2a;
|
||||||
|
--secondary-foreground: #fafafa;
|
||||||
|
--muted: #1e1e1e;
|
||||||
|
--muted-foreground: #a1a1aa;
|
||||||
|
--accent: #6b7280;
|
||||||
|
--accent-foreground: #ffffff;
|
||||||
|
--destructive: #ef4444;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--border: #333333;
|
||||||
|
--input: #333333;
|
||||||
|
--ring: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #0a0a0a;
|
||||||
|
--card: #ffffff;
|
||||||
|
--card-foreground: #0a0a0a;
|
||||||
|
--popover: #ffffff;
|
||||||
|
--popover-foreground: #0a0a0a;
|
||||||
|
--primary: #0a0a0a;
|
||||||
|
--primary-foreground: #ffffff;
|
||||||
|
--secondary: #f5f5f5;
|
||||||
|
--secondary-foreground: #0a0a0a;
|
||||||
|
--muted: #f5f5f5;
|
||||||
|
--muted-foreground: #737373;
|
||||||
|
--accent: #6b7280;
|
||||||
|
--accent-foreground: #ffffff;
|
||||||
|
--destructive: #ef4444;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--border: #e5e5e5;
|
||||||
|
--input: #e5e5e5;
|
||||||
|
--ring: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
sans-serif;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: #24292e;
|
||||||
|
border-color: #24292e;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(36, 41, 46, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover {
|
||||||
|
background: #24292e;
|
||||||
|
border-color: #24292e;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(36, 41, 46, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||