Merge pull request #336 from nettanvirdev/main

Update: Updated The AgenticSeek Frontend
This commit is contained in:
Martin
2025-06-20 22:50:03 +02:00
committed by GitHub
14 changed files with 1485 additions and 655 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because it is too large Load Diff
+401 -312
View File
@@ -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;
+55 -54
View File
@@ -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;
};
+10 -6
View File
@@ -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>
); );
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

-1
View File
@@ -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;
}