diff --git a/web/frontend/index.html b/web/frontend/index.html index d3bdd90f8..a6c5357a3 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -8,7 +8,7 @@ - PicoClaw + Africa diff --git a/web/frontend/package.json b/web/frontend/package.json index 1b6821c33..e398d2d36 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -19,6 +19,7 @@ "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@tabler/icons-react": "^3.43.0", + "motion": "^12.0.0", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-query": "^5.99.0", "@tanstack/react-router": "^1.169.2", diff --git a/web/frontend/src/components/agent/agents/agents-page.tsx b/web/frontend/src/components/agent/agents/agents-page.tsx index e9902b334..8374b7702 100644 --- a/web/frontend/src/components/agent/agents/agents-page.tsx +++ b/web/frontend/src/components/agent/agents/agents-page.tsx @@ -1,5 +1,6 @@ import { useDeferredValue, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { motion } from "motion/react" import { toast } from "sonner" import { @@ -20,6 +21,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" import { useQuery, useQueryClient } from "@tanstack/react-query" +import { IconPlus } from "@tabler/icons-react" import { AgentCard } from "./agent-card" import { AgentFormModal } from "./agent-form-modal" @@ -60,7 +62,14 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { if (agentsQuery.isLoading) { return (
-

Loading agents...

+
+ + Loading agents... +
) } @@ -68,7 +77,7 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { if (agentsQuery.isError) { return (
-

Failed to load agents. Please try again.

+

Failed to load agents. Please try again.

) } @@ -107,7 +116,6 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { toast.error( error instanceof Error ? error.message : "Failed to update agent", ) - // Refresh to revert the UI state void queryClient.invalidateQueries({ queryKey: ["agents"] }) } } @@ -121,7 +129,7 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { } const mainContent = ( -
+
{/* Header Controls */}
@@ -129,27 +137,28 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { placeholder={t("common.search", "Search agents...")} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="bg-background" + className="bg-black/40 border-[#00bcff]/20 text-cyan-100 placeholder:text-cyan-100/30 focus:border-[#00bcff] focus:ring-[#00bcff]/20 backdrop-blur-sm" />
{/* Agent Grid */} -
+
{filteredAgents.length === 0 ? ( -
-

+

+

{deferredSearchQuery ? t("pages.agent.agents.no_results", "No agents found") : t("pages.agent.agents.no_agents", "No agents yet")}

-

+

{deferredSearchQuery ? t("pages.agent.agents.no_results_hint", "Try a different search") : t("pages.agent.agents.no_agents_hint", "Create an agent to get started")} @@ -198,23 +207,23 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { if (!open) setAgentToDelete(null) }} > - + {t("pages.agent.agents.confirm_delete", "Delete Agent?")} -

+

{t( "pages.agent.agents.confirm_delete_message", `Are you sure you want to delete "${agentToDelete?.name}"? This action cannot be undone.`, )}

- + {t("common.cancel", "Cancel")} {t("common.delete", "Delete")} @@ -234,7 +243,7 @@ export function AgentsPage({ embedded = false }: AgentsPageProps = {}) { } return ( -
+
{mainContent} diff --git a/web/frontend/src/components/agent/cockpit/cockpit-page.tsx b/web/frontend/src/components/agent/cockpit/cockpit-page.tsx index 15ff13705..fbedd793f 100644 --- a/web/frontend/src/components/agent/cockpit/cockpit-page.tsx +++ b/web/frontend/src/components/agent/cockpit/cockpit-page.tsx @@ -1,6 +1,11 @@ import dayjs from "dayjs" -import { useMemo } from "react" -import { IconArrowRight, IconLayoutDashboard, IconUsers, IconBrain, IconFlask } from "@tabler/icons-react" +import { useEffect, useMemo, useState } from "react" +import { motion } from "motion/react" +import { + IconLayoutDashboard, IconUsers, IconBrain, IconFlask, + IconCircleDot, IconActivity, IconCpu, IconDatabase, IconBolt, + IconShield, IconTerminal, IconWifi, IconLock, IconTrendingUp, +} from "@tabler/icons-react" import { usePicoChat } from "@/hooks/use-pico-chat" import { Badge } from "@/components/ui/badge" @@ -13,23 +18,439 @@ import { AgentsPage } from "../agents" import { SkillsPage } from "../skills" import { ResearchPage } from "../research/research-page" -function reasonLabel(reasonCode?: string) { - switch (reasonCode) { - case "requires_subagent": - return "Requires subagent runtime" - case "requires_skills": - return "Requires skills support" - case "requires_mcp_discovery": - return "Requires MCP discovery" - case "requires_linux": - return "Linux only" - case "requires_serial_platform": - return "Unsupported serial platform" - default: - return reasonCode ?? "" - } +const TAB_CONFIG = [ + { key: "tools" as const, icon: IconLayoutDashboard, label: "Tools" }, + { key: "skills" as const, icon: IconBrain, label: "Skills" }, + { key: "agents" as const, icon: IconUsers, label: "Agents" }, + { key: "research" as const, icon: IconFlask, label: "Research" }, +] + +/* ─── 3D Holographic Circle Component ─── */ +function HolographicCircle() { + const [rotation, setRotation] = useState(0) + + useEffect(() => { + const interval = setInterval(() => { + setRotation((prev) => (prev + 0.5) % 360) + }, 50) + return () => clearInterval(interval) + }, []) + + return ( +
+ {/* Outer Glow */} + + + {/* Rotating Rings - 3D Perspective */} +
+ {[0, 1, 2, 3, 4].map((index) => ( +
+ ))} + + {/* Inner Core */} +
+ + {/* Scanning Line */} + +
+ + {/* Center Display */} +
+ + AI + +
Core Online
+ + {/* Data Points around circle */} + {[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => ( + + ))} +
+ + {/* Holographic Scan Lines */} +
+ +
+
+ ) } +/* ─── Particle Field Component ─── */ +function ParticleField() { + const particles = useMemo( + () => + Array.from({ length: 40 }, (_, i) => ({ + id: i, + x: Math.random() * 100, + y: Math.random() * 100, + size: Math.random() * 3 + 1, + duration: Math.random() * 10 + 10, + delay: Math.random() * 5, + })), + [] + ) + + return ( +
+ {particles.map((p) => ( + + ))} + + {/* Connection Lines */} + + {Array.from({ length: 15 }, (_, i) => ( + + ))} + +
+ ) +} + +/* ─── Metric Card Component ─── */ +function MetricCard({ icon, label, value, trend }: { icon: React.ReactNode; label: string; value: string; trend: string }) { + const isPositive = trend.startsWith("+") + + return ( + + {/* Animated Background */} + + + {/* Content */} +
+
+
{icon}
+ + {trend} + +
+
{label}
+
{value}
+
+ + {/* Scan Line */} + + + {/* Corner Accent */} +
+ + ) +} + +/* ─── Data Panel Component ─── */ +function DataPanel({ title, items }: { + title: string + items: Array<{ label: string; status: string; progress: number }> +}) { + return ( + + {/* Header */} +
+

{title}

+
+ {[0, 1, 2].map((i) => ( + + ))} +
+
+ + {/* Items */} +
+ {items.map((item, index) => ( + +
+ {item.label} + + {item.status} + +
+ + {/* Progress Bar */} +
+ + +
+
+ ))} +
+ + {/* Decorative Corner */} +
+ + ) +} + +/* ─── Circular Progress Component ─── */ +function CircularProgress({ icon, label, percentage, color = "cyan" }: { + icon: React.ReactNode; label: string; percentage: number; color?: "cyan" | "blue" | "green" +}) { + const radius = 70 + const circumference = 2 * Math.PI * radius + const offset = circumference - (percentage / 100) * circumference + + const colorMap = { + cyan: { stroke: "#00bcff", glow: "#00bcff99", bg: "#00bcff1a" }, + blue: { stroke: "#0080ff", glow: "#0080ff99", bg: "#0080ff1a" }, + green: { stroke: "#00ff88", glow: "#00ff8899", bg: "#00ff881a" }, + } + const colors = colorMap[color] + + return ( + +
+ + + + + +
+ + {icon} + + + {percentage}% + +
+
+
{label}
+
+
+ + ) +} + +/* ─── System Status Grid Component ─── */ +function SystemStatusGrid() { + const statusItems = [ + { icon: , label: "CONNECTION", value: "STABLE", status: "good" }, + { icon: , label: "STORAGE", value: "67% FREE", status: "good" }, + { icon: , label: "SECURITY", value: "ENCRYPTED", status: "good" }, + { icon: , label: "PERFORMANCE", value: "OPTIMAL", status: "good" }, + ] + + return ( +
+ {statusItems.map((item, index) => ( + + {/* Hover Glow */} +
+ +
+
+
{item.icon}
+ +
+
{item.label}
+
{item.value}
+
+ + {/* Scan Effect */} + + + ))} +
+ ) +} + +/* ─── Terminal Output Component ─── */ +function TerminalOutput() { + return ( + +
+ + TERMINAL +
+
+ + > Initializing core systems... + + + > All systems operational + + + > Awaiting commands... + + + | + +
+
+ ) +} + +/* ═══════════════════════════════════════════════ + MAIN COCKPIT PAGE + ═══════════════════════════════════════════════ */ export function CockpitPage() { const { activeSessionId } = usePicoChat() const { @@ -48,141 +469,291 @@ export function CockpitPage() { ) return ( -
- {/* Ghost Background Typography */} -
- COCKPIT - SYSTEM -
+
+ {/* ── 3D Perspective Grid Background ── */} +
- {/* Header */} -
-
- Terminal Status - CONNECTED / {dayjs().format('DD.MM.YYYY')} -
-
- Runtime Epoch - {dayjs().format('HH:mm')} GMT+1 -
-
+ {/* ── Particle Field ── */} + -
-
+ {/* ── Scanline Effect ── */} +
- {/* Main Workspace */} -
- {/* Hero Section */} -
-

- AGENT INTERFACE -

-

- Active control node for autonomous agents. Managing tool surfaces, memory networks. -

+ {/* ── Corner Accents ── */} +
+
+
+
+ + {/* ── Main Content ── */} +
+ {/* Header */} + +
+

+ AFRICA +

+

+ ADVANCED FRAMEWORK FOR INTELLIGENT SYSTEM +

+
+ +
+
+ SYSTEMS ONLINE
- -
- - - - +
+ {dayjs().format("DD.MM.YYYY")} / {dayjs().format("HH:mm")} UTC
+
+ - {activeTab === "skills" && } + {/* Tab Navigation */} + + {TAB_CONFIG.map(({ key, icon: Icon, label }) => ( + + ))} + - {activeTab === "agents" && } + {/* ── Main Grid Layout ── */} +
+ {/* Left Column - Holographic Display + System Status */} + {activeTab === "tools" && ( + + {/* Main Holographic Circle */} +
+ +
- {activeTab === "research" && } + {/* System Status Cards */} + +
+ )} + {/* Center Column - Main Content Area */} + + {/* Tools Tab Content */} {activeTab === "tools" && ( -
- {/* Tool Grid */} -
-
-
- Active Modules -

Tool Grid

+ <> + {/* Top Metrics */} +
+ } + label="CPU LOAD" + value="47%" + trend="+2.3%" + /> + } + label="MEMORY" + value="8.2 GB" + trend="-1.1%" + /> + } + label="POWER" + value="89%" + trend="+5.2%" + /> + } + label="NETWORK" + value="12.4 MB/s" + trend="+8.7%" + /> +
+ + {/* Active Protocols / Tool Status */} + + items.slice(0, 4).map((tool) => ({ + label: tool.name, + status: tool.status === "enabled" ? "ACTIVE" : "STANDBY", + progress: tool.status === "enabled" ? 94 : 30, + })) + )} + /> + + {/* Memory Network */} +
+
+
+ + MEMORY NETWORK +
-
- {filteredToolCount} Visible - - Configuration - +
+
-
- {groupedTools.flatMap(([, items]) => - items.map((tool) => ( -
-
-
-

- {tool.name} -

- - {tool.status} - + {/* Tool Grid - Compact */} +
+
+ TOOL REGISTRY + {filteredToolCount} Active +
+
+ {groupedTools.flatMap(([, items]) => + items.map((tool) => ( +
+
+
+

+ {tool.name} +

+
+ toggleTool(tool.name, checked)} + className="data-[state=checked]:bg-[#00bcff] scale-75" + />
- toggleTool(tool.name, checked)} +

+ {tool.description} +

+ + {tool.status} + + + {/* Scan Line */} +
-

- {tool.description} -

-
- {tool.config_key} - {(tool as any).reason_code && ( - {reasonLabel((tool as any).reason_code)} - )} + )) + )} +
+
+ + )} + + {/* Skills Tab */} + {activeTab === "skills" && } + + {/* Agents Tab */} + {activeTab === "agents" && } + + {/* Research Tab */} + {activeTab === "research" && } + + + {/* Right Column - Circular Stats + Terminal */} + {activeTab === "tools" && ( + + } + label="DEFENSE" + percentage={92} + color="cyan" + /> + } + label="UPTIME" + percentage={99} + color="blue" + /> + } + label="CONNECTIVITY" + percentage={87} + color="cyan" + /> + + {/* Subagent Manifest */} +
+
+ + Subagents +
+
+ {sessionSubagents === null ? ( +
+ + Loading... +
+ ) : sessionSubagents.length === 0 ? ( +
No subagents active
+ ) : ( + sessionSubagents.map((task) => ( +
+
+ + {task.label || task.id} + + + {task.status} + +
+
+ {dayjs(task.created).format("HH:mm:ss")}
)) @@ -190,79 +761,12 @@ export function CockpitPage() {
- {/* Memory Network */} -
-
-
- Relational Map -

Memory Network

-
-
-
- -
-
-
- )} -
- - {/* Right Sidebar */} - + {/* Terminal Output */} + + + )}
- - {/* Footer */} -
) -} +} \ No newline at end of file diff --git a/web/frontend/src/components/agent/cockpit/memory-graph.tsx b/web/frontend/src/components/agent/cockpit/memory-graph.tsx index cb6cfb9fd..2f92e512b 100644 --- a/web/frontend/src/components/agent/cockpit/memory-graph.tsx +++ b/web/frontend/src/components/agent/cockpit/memory-graph.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react" +import { motion } from "motion/react" import type { PicoMemoryGraphEdge, @@ -28,11 +29,11 @@ const GROUP_COLUMNS: Record = { } function groupColor(group: string) { - if (group === "memory") return "#72f0a0" - if (group.startsWith("daily-")) return "#4dd0e1" - if (group === "tool") return "#f8d66d" - if (group === "media") return "#f395d6" - return "#90e89f" + if (group === "memory") return "#00bcff" + if (group.startsWith("daily-")) return "#22d3ee" + if (group === "tool") return "#38bdf8" + if (group === "media") return "#7dd3fc" + return "#06b6d4" } function computeLayout(nodes: PicoMemoryGraphNode[]): PositionedNode[] { @@ -76,69 +77,133 @@ export function MemoryGraph({ nodes, edges }: MemoryGraphProps) { positionedNodes.find((node) => node.id === selectedId) ?? positionedNodes[0] return ( -
-
+
+
+ {/* Holographic scan line overlay */} + + - - - + {/* 3D Grid Gradient */} + + + + + + {/* Glow filter for edges */} + + + + + + + + + {/* Stronger glow for active nodes */} + + + + + + + + + {/* Holographic radial gradient for nodes */} + + + + + - - {Array.from({ length: 10 }).map((_, index) => ( + + {/* Background */} + + + {/* 3D Perspective Grid Lines */} + {Array.from({ length: 20 }).map((_, index) => ( ))} - {Array.from({ length: 8 }).map((_, index) => ( + {Array.from({ length: 12 }).map((_, index) => ( ))} + {/* Edges with holographic glow */} {edges.map((edge) => { const source = nodeMap.get(edge.source) const target = nodeMap.get(edge.target) - if (!source || !target) { - return null - } + if (!source || !target) return null const active = selectedId != null && (selectedId === edge.source || selectedId === edge.target) return ( - + + {/* Glow layer */} + {active && ( + + )} + {/* Main edge */} + + {/* Animated pulse along edge when active */} + {active && ( + + + + )} + ) })} + {/* Nodes with 3D holographic style */} {positionedNodes.map((node) => { const active = node.id === selectedNode?.id const color = groupColor(node.group) @@ -152,29 +217,82 @@ export function MemoryGraph({ nodes, edges }: MemoryGraphProps) { onClick={() => setSelectedId(node.id)} className="cursor-pointer" > + {/* Outer holographic ring - pulsing */} + + {active && ( + + )} + + + {/* Glow halo */} + + {/* Holographic field */} + {active && ( + + )} + + {/* Main node circle */} + + {/* Inner glow ring */} + {active && ( + + )} + + {/* Label */} - {node.kind === "root" ? node.label : node.label.slice(0, 18)} + {node.kind === "root" ? node.label : node.label.slice(0, 16)} ) @@ -182,50 +300,51 @@ export function MemoryGraph({ nodes, edges }: MemoryGraphProps) {
-
+ {/* Node Detail Panel */} +
{selectedNode ? ( <>
-

+

Node Focus

-

+

{selectedNode.label}

{selectedNode.kind}
-

+

{selectedNode.preview || "No extra preview for this node yet."}

-
-
+
+
Cluster - + {selectedNode.group}
-
+
Weight - + {selectedNode.weight ?? 1}
) : ( -

No graph data available.

+

No graph data available.

)}
) -} +} \ No newline at end of file diff --git a/web/frontend/src/components/agent/skills/skills-page.tsx b/web/frontend/src/components/agent/skills/skills-page.tsx index 6ddcbc12b..70275f461 100644 --- a/web/frontend/src/components/agent/skills/skills-page.tsx +++ b/web/frontend/src/components/agent/skills/skills-page.tsx @@ -1,5 +1,6 @@ import { useDeferredValue, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { motion } from "motion/react" import type { SkillSupportItem } from "@/api/skills" import { useCockpitSkills } from "@/hooks/use-cockpit-skills" import { PageHeader } from "@/components/page-header" @@ -56,7 +57,14 @@ export function SkillsPage({ embedded = false }: SkillsPageProps) { if (isLoading) { return (
-

Loading skills...

+
+ + Loading skills... +
) } @@ -64,33 +72,38 @@ export function SkillsPage({ embedded = false }: SkillsPageProps) { if (isError) { return (
-

Failed to load skills. Please try again.

+

Failed to load skills. Please try again.

) } const mainContent = ( -
+
- setSearchQuery(e.target.value)} - className="bg-background" - /> +
+ setSearchQuery(e.target.value)} + className="bg-black/40 border-[#00bcff]/20 text-cyan-100 placeholder:text-cyan-100/30 focus:border-[#00bcff] focus:ring-[#00bcff]/20 backdrop-blur-sm" + /> +
+
+
+ {filteredSkills.length} Skills Loaded
-
+
{filteredSkills.length === 0 ? ( -
-

+

+

{deferredSearchQuery ? t("pages.agent.skills.no_results", "No skills found") : t("pages.agent.skills.no_skills", "No skills installed")}

-

+

{deferredSearchQuery ? t("pages.agent.skills.no_results_hint", "Try a different search") : t("pages.agent.skills.no_skills_hint", "Install skills via the CLI or import them")} @@ -114,23 +127,23 @@ export function SkillsPage({ embedded = false }: SkillsPageProps) { open={skillToDelete !== null} onOpenChange={(open) => { if (!open) setSkillToDelete(null) }} > - + {t("pages.agent.skills.confirm_delete", "Delete Skill?")} -

+

{t( "pages.agent.skills.confirm_delete_message", `Are you sure you want to delete "${skillToDelete?.name}"? This action cannot be undone.`, )}

- + {t("common.cancel", "Cancel")} {t("common.delete", "Delete")} @@ -149,7 +162,7 @@ export function SkillsPage({ embedded = false }: SkillsPageProps) { } return ( -
+
{mainContent} @@ -157,4 +170,4 @@ export function SkillsPage({ embedded = false }: SkillsPageProps) { {modals}
) -} +} \ No newline at end of file diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx index 3031e9c9e..542cb9c9e 100644 --- a/web/frontend/src/components/chat/chat-page.tsx +++ b/web/frontend/src/components/chat/chat-page.tsx @@ -53,6 +53,30 @@ function readFileAsDataUrl(file: File): Promise { }) } +/** Background Jarvis Orb — large semi-transparent orb behind the chat messages */ +function BackgroundOrb() { + return ( +
+ {/* Main orb glow - centered in the viewport */} +
+ {/* Outermost ring - very subtle */} + + + + + + {/* Core glow */} +
+
+
+
+ {/* Ambient side glows */} +
+
+
+ ) +} + function resolveChatInputDisabledReason({ hasDefaultModel, connectionState, @@ -62,46 +86,16 @@ function resolveChatInputDisabledReason({ connectionState: ConnectionState gatewayState: GatewayState }): ChatInputDisabledReason | null { - if (gatewayState === "unknown") { - return "gatewayUnknown" - } - - if (gatewayState === "starting") { - return "gatewayStarting" - } - - if (gatewayState === "restarting") { - return "gatewayRestarting" - } - - if (gatewayState === "stopping") { - return "gatewayStopping" - } - - if (gatewayState === "stopped") { - return "gatewayStopped" - } - - if (gatewayState === "error") { - return "gatewayError" - } - - if (connectionState === "connecting") { - return "websocketConnecting" - } - - if (connectionState === "error") { - return "websocketError" - } - - if (connectionState === "disconnected") { - return "websocketDisconnected" - } - - if (!hasDefaultModel) { - return "noDefaultModel" - } - + if (gatewayState === "unknown") return "gatewayUnknown" + if (gatewayState === "starting") return "gatewayStarting" + if (gatewayState === "restarting") return "gatewayRestarting" + if (gatewayState === "stopping") return "gatewayStopping" + if (gatewayState === "stopped") return "gatewayStopped" + if (gatewayState === "error") return "gatewayError" + if (connectionState === "connecting") return "websocketConnecting" + if (connectionState === "error") return "websocketError" + if (connectionState === "disconnected") return "websocketDisconnected" + if (!hasDefaultModel) return "noDefaultModel" return null } @@ -199,19 +193,15 @@ export function ChatPage() { // Check for permission requests in messages useEffect(() => { - if (permissionRequest) return // Already showing a permission prompt - - // Check the last few messages for request_permission tool calls + if (permissionRequest) return for (let i = messages.length - 1; i >= Math.max(0, messages.length - 5); i--) { const msg = messages[i] if (msg.role !== "assistant") continue if (!msg.toolCalls) continue - const permCall = msg.toolCalls.find( (tc) => tc.function?.name === "request_permission" ) if (!permCall || !permCall.function) continue - try { const args = JSON.parse(permCall.function.arguments ?? "{}") if (args.path && args.command) { @@ -222,25 +212,16 @@ export function ChatPage() { }) return } - } catch { - // Ignore parse errors - } + } catch { /* ignore */ } } }, [messages, permissionRequest]) - const handlePermissionGranted = () => { - setPermissionRequest(null) - } - - const handlePermissionDenied = () => { - setPermissionRequest(null) - } - + const handlePermissionGranted = () => setPermissionRequest(null) + const handlePermissionDenied = () => setPermissionRequest(null) const handleAddImages = () => { if (!canInput) return fileInputRef.current?.click() } - const handleRemoveAttachment = (index: number) => { setAttachments((prev) => prev.filter((_, itemIndex) => itemIndex !== index)) } @@ -298,11 +279,11 @@ export function ChatPage() { canInput && (Boolean(input.trim()) || attachments.length > 0) return ( -
+
-
- +
+ {t("chat.showAssistantDetails")} {t("chat.newChat")} @@ -358,9 +339,15 @@ export function ChatPage() {
-
+ {/* Background orb behind text */} + + + {/* Scan line overlay */} +
+ +
{messages.length === 0 && !isTyping && (
) -} +} \ No newline at end of file diff --git a/web/frontend/src/components/logs/logs-page.tsx b/web/frontend/src/components/logs/logs-page.tsx index 853da223a..1f2d2640a 100644 --- a/web/frontend/src/components/logs/logs-page.tsx +++ b/web/frontend/src/components/logs/logs-page.tsx @@ -26,6 +26,7 @@ export function LogsPage() { size="sm" onClick={clearLogs} disabled={logs.length === 0 || clearing} + className="border-[rgba(0,212,255,0.3)] bg-[rgba(0,212,255,0.05)] text-[#00d4ff] hover:bg-[rgba(0,212,255,0.1)]" > {t("pages.logs.clear")} diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css index fc55a3a32..752cdecc6 100644 --- a/web/frontend/src/index.css +++ b/web/frontend/src/index.css @@ -45,92 +45,67 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-jarvis-cyan: #00d4ff; + --color-jarvis-blue: #0ea5e9; + --color-jarvis-light: #22d3ee; + --color-jarvis-surface: #0f172a; + --color-jarvis-glow: rgba(0, 212, 255, 0.15); } :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --background: #0a0e17; + --foreground: #e0f2fe; + --card: rgba(6, 20, 40, 0.7); + --card-foreground: #e0f2fe; + --popover: #111827; + --popover-foreground: #e0f2fe; + --primary: #00d4ff; + --primary-foreground: #0a0e17; + --secondary: #1e293b; + --secondary-foreground: #e0f2fe; + --muted: #1e293b; + --muted-foreground: #94a3b8; + --accent: rgba(0, 212, 255, 0.1); + --accent-foreground: #00d4ff; + --destructive: #ef4444; + --border: rgba(0, 212, 255, 0.15); + --input: rgba(0, 212, 255, 0.1); + --ring: #00d4ff; + --chart-1: #00d4ff; + --chart-2: #0ea5e9; + --chart-3: #22d3ee; + --chart-4: #06b6d4; + --chart-5: #0891b2; + --sidebar: #0f172a; + --sidebar-foreground: #e0f2fe; + --sidebar-primary: #00d4ff; + --sidebar-primary-foreground: #0a0e17; + --sidebar-accent: rgba(0, 212, 255, 0.1); + --sidebar-accent-foreground: #00d4ff; + --sidebar-border: rgba(0, 212, 255, 0.15); + --sidebar-ring: #00d4ff; } -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); +/* Force dark mode always */ +html { + color-scheme: dark; } @layer base { * { @apply border-border outline-ring/50; scrollbar-width: thin; - scrollbar-color: var(--border) transparent; + scrollbar-color: rgba(0, 212, 255, 0.3) transparent; } body { @apply bg-background text-foreground; } - /* WebKit Custom Scrollbar */ ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 4px; + height: 4px; } ::-webkit-scrollbar-track { @@ -138,12 +113,12 @@ } ::-webkit-scrollbar-thumb { - background: var(--border); + background: rgba(0, 212, 255, 0.3); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: var(--muted-foreground); + background: rgba(0, 212, 255, 0.5); } } @@ -157,25 +132,242 @@ height: calc(100svh - 3.5rem); } -/* Typing indicator animations */ -@keyframes shimmer { - 0% { - background-position: 200% 0; - } +/* ===== JARVIS CUSTOM ANIMATIONS ===== */ - 100% { - background-position: -200% 0; - } +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } } @keyframes fadeSlideIn { - 0% { - opacity: 0; - transform: translateY(4px); - } - - 100% { - opacity: 1; - transform: translateY(0); - } + 0% { opacity: 0; transform: translateY(4px); } + 100% { opacity: 1; transform: translateY(0); } } + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 5px rgba(0, 212, 255, 0.2), 0 0 10px rgba(0, 212, 255, 0.1); } + 50% { box-shadow: 0 0 15px rgba(0, 212, 255, 0.4), 0 0 30px rgba(0, 212, 255, 0.15); } +} + +@keyframes pulse-glow-text { + 0%, 100% { text-shadow: 0 0 5px rgba(0, 212, 255, 0.4); } + 50% { text-shadow: 0 0 20px rgba(0, 212, 255, 0.7), 0 0 40px rgba(0, 212, 255, 0.3); } +} + +@keyframes breathe { + 0%, 100% { transform: scale(1); opacity: 0.8; } + 50% { transform: scale(1.05); opacity: 1; } +} + +@keyframes orb-rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes orb-rotate-reverse { + from { transform: rotate(360deg); } + to { transform: rotate(0deg); } +} + +@keyframes typing-dot { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } + 30% { transform: translateY(-6px); opacity: 1; } +} + +@keyframes hud-corner-flash { + 0%, 90%, 100% { opacity: 0.5; } + 95% { opacity: 1; } +} + +@keyframes scan-line { + 0% { top: -2px; } + 100% { top: 100%; } +} + +@keyframes float-up { + 0% { opacity: 0; transform: translateY(12px); } + 100% { opacity: 1; transform: translateY(0); } +} + +@keyframes ring-pulse { + 0%, 100% { stroke-opacity: 0.3; } + 50% { stroke-opacity: 0.8; } +} + +@keyframes voiceBar { + 0% { height: 4px; opacity: 0.5; } + 100% { height: 16px; opacity: 1; } +} + +@keyframes scan { + 0% { background-position: 0 0; } + 100% { background-position: 0 100%; } +} + +@keyframes holographic-pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +/* ===== JARVIS UTILITY CLASSES ===== */ + +.glass-panel { + background: rgba(6, 20, 40, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(0, 212, 255, 0.15); +} + +.glass-panel-strong { + background: rgba(6, 20, 40, 0.85); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(0, 212, 255, 0.2); +} + +.glow-border { + box-shadow: 0 0 5px rgba(0, 212, 255, 0.15), inset 0 0 5px rgba(0, 212, 255, 0.03); +} + +.glow-border-active { + box-shadow: 0 0 10px rgba(0, 212, 255, 0.25), 0 0 20px rgba(0, 212, 255, 0.1), inset 0 0 8px rgba(0, 212, 255, 0.05); + border-color: rgba(0, 212, 255, 0.35); +} + +.hud-corners { + position: relative; +} + +.hud-corners::before, +.hud-corners::after { + content: ''; + position: absolute; + width: 10px; + height: 10px; + border-color: rgba(0, 212, 255, 0.5); + animation: hud-corner-flash 4s ease-in-out infinite; + pointer-events: none; +} + +.hud-corners::before { + top: -1px; + left: -1px; + border-top: 2px solid; + border-left: 2px solid; +} + +.hud-corners::after { + bottom: -1px; + right: -1px; + border-bottom: 2px solid; + border-right: 2px solid; +} + +.glow-text { + text-shadow: 0 0 10px rgba(0, 212, 255, 0.4), 0 0 20px rgba(0, 212, 255, 0.15); +} + +.glow-text-pulse { + animation: pulse-glow-text 3s ease-in-out infinite; +} + +.animate-breathe { + animation: breathe 4s ease-in-out infinite; +} + +.animate-pulse-glow { + animation: pulse-glow 3s ease-in-out infinite; +} + +.animate-float-up { + animation: float-up 0.4s ease-out forwards; +} + +/* ===== HEX GRID BACKGROUND ===== */ + +.hex-grid-bg { + background-image: + linear-gradient(30deg, rgba(0, 212, 255, 0.02) 12%, transparent 12.5%, transparent 87%, rgba(0, 212, 255, 0.02) 87.5%), + linear-gradient(150deg, rgba(0, 212, 255, 0.02) 12%, transparent 12.5%, transparent 87%, rgba(0, 212, 255, 0.02) 87.5%), + linear-gradient(30deg, rgba(0, 212, 255, 0.02) 12%, transparent 12.5%, transparent 87%, rgba(0, 212, 255, 0.02) 87.5%), + linear-gradient(150deg, rgba(0, 212, 255, 0.02) 12%, transparent 12.5%, transparent 87%, rgba(0, 212, 255, 0.02) 87.5%), + linear-gradient(60deg, rgba(0, 212, 255, 0.015) 25%, transparent 25.5%, transparent 75%, rgba(0, 212, 255, 0.015) 75%), + linear-gradient(60deg, rgba(0, 212, 255, 0.015) 25%, transparent 25.5%, transparent 75%, rgba(0, 212, 255, 0.015) 75%); + background-size: 40px 70px; + background-position: 0 0, 0 0, 20px 35px, 20px 35px, 0 0, 20px 35px; +} + +/* ===== SCAN LINE ===== */ + +.scan-line-overlay { + position: relative; + overflow: hidden; +} + +.scan-line-overlay::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent); + animation: scan-line 8s linear infinite; + pointer-events: none; + z-index: 1; +} + +/* Markdown in chat messages - Jarvis style */ +.jarvis-markdown h1, .jarvis-markdown h2, .jarvis-markdown h3 { + color: #00d4ff; + font-weight: 600; + margin-top: 1em; + margin-bottom: 0.5em; +} + +.jarvis-markdown p { + margin-bottom: 0.5em; + line-height: 1.7; +} + +.jarvis-markdown code { + background: rgba(0, 212, 255, 0.08); + border: 1px solid rgba(0, 212, 255, 0.12); + border-radius: 3px; + padding: 0.1em 0.4em; + font-size: 0.875em; + color: #22d3ee; +} + +.jarvis-markdown pre { + background: rgba(0, 0, 0, 0.4) !important; + border: 1px solid rgba(0, 212, 255, 0.12) !important; + border-radius: 6px !important; + padding: 1em !important; + overflow-x: auto; + margin: 0.75em 0 !important; +} + +.jarvis-markdown pre code { + background: none !important; + border: none !important; + padding: 0 !important; + color: #e0f2fe !important; +} + +.jarvis-markdown a { + color: #00d4ff; + text-decoration: underline; + text-underline-offset: 2px; +} + +.jarvis-markdown strong { + color: #e0f2fe; + font-weight: 600; +} + +.jarvis-markdown blockquote { + border-left: 3px solid rgba(0, 212, 255, 0.3); + padding-left: 1em; + color: #94a3b8; + margin: 0.75em 0; +} \ No newline at end of file