mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-14 00:47:52 +00:00
feat(ui): Raycast-inspired visual refresh with System Pulse and X-Ray footers (#57)
* feat(ui): update design tokens — system-ui font, warmer palette, accent triad - Remove Merriweather serif font, switch to system-ui font stack - Warmer dark mode neutrals (#161618 bg instead of #09090b) - Add amber/purple accent tokens for signature color triad - Update borders to rgba for softer feel - Tighter line-height (1.5) and letter-spacing (-0.01em) - Update PWA theme_color to match new palette Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): frosted glass sidebar with backdrop-blur Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): initialize shadcn/ui with button, dialog, input, select, tooltip, sonner Set up shadcn/ui (v4) for Tailwind CSS v4 with CSS-first config. Added path aliases (@/) to tsconfig.json and vite.config.ts. Installed six components (button, dialog, input, select, tooltip, sonner) and wired Sonner's <Toaster> into App.tsx for toast notifications. Removed the next-themes dependency since this is a Vite/React project, not Next.js. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): disable create agent when unavailable, replace errors with toasts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): add System Pulse bar and health check banner - 3px gradient bar at top reflecting system state (idle/inferencing/agent) - Health check banner when backend is unreachable - Pulse colors: blue (inference), purple (agents), dim blue (idle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): add X-Ray message footers with collapsible telemetry - MessageTelemetry type on ChatMessage (engine, model, speed, TTFT, timing) - Capture timing data during SSE streaming in InputArea - XRayFooter component: collapsed one-liner + expandable trace grid - Integrated into MessageBubble, replacing old token count display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): resolve shadcn/ui CSS token conflicts with design system - Namespace shadcn @theme inline variables (--color-cn-*) to avoid overriding project's --color-border, --color-accent, --radius-* tokens - Remove @apply border-border, bg-background, text-foreground, font-sans that conflicted with our explicit CSS custom properties - Remove dead Geist font import (we use system-ui) - Remove ---break--- comment artifacts from shadcn CLI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0dac51c649
commit
e240fde74a
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
+1
-4
@@ -3,13 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#09090b" />
|
||||
<meta name="theme-color" content="#161618" />
|
||||
<meta name="description" content="OpenJarvis — on-device AI assistant" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap" rel="stylesheet" />
|
||||
<title>OpenJarvis</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
+3287
-80
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-autostart": "^2",
|
||||
@@ -19,6 +21,8 @@
|
||||
"@tauri-apps/plugin-process": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.38",
|
||||
"lucide-react": "^0.576.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -30,7 +34,11 @@
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shadcn": "^4.0.7",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AgentsPage } from './pages/AgentsPage';
|
||||
import { LogsPage } from './pages/LogsPage';
|
||||
import { CommandPalette } from './components/CommandPalette';
|
||||
import { SetupScreen } from './components/SetupScreen';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { useAppStore } from './lib/store';
|
||||
import { fetchModels, fetchServerInfo, fetchSavings, submitSavings, isTauri } from './lib/api';
|
||||
import { OptInModal } from './components/OptInModal';
|
||||
@@ -138,6 +139,7 @@ export default function App() {
|
||||
<Route path="logs" element={<LogsPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster position="bottom-right" />
|
||||
{commandPaletteOpen && <CommandPalette />}
|
||||
{optInModalOpen && (
|
||||
<OptInModal onClose={() => setOptInModalOpen(false)} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { streamChat } from '../../lib/sse';
|
||||
import { fetchSavings } from '../../lib/api';
|
||||
import { MicButton } from './MicButton';
|
||||
import { useSpeech } from '../../hooks/useSpeech';
|
||||
import type { ChatMessage, ToolCallInfo, TokenUsage } from '../../types';
|
||||
import type { ChatMessage, ToolCallInfo, TokenUsage, MessageTelemetry } from '../../types';
|
||||
|
||||
export function InputArea() {
|
||||
const [input, setInput] = useState('');
|
||||
@@ -129,6 +129,7 @@ export function InputArea() {
|
||||
let usage: TokenUsage | undefined;
|
||||
const toolCalls: ToolCallInfo[] = [];
|
||||
let lastFlush = 0;
|
||||
let ttftMs: number | undefined;
|
||||
|
||||
setStreamState({
|
||||
isStreaming: true,
|
||||
@@ -202,6 +203,7 @@ export function InputArea() {
|
||||
const delta = data.choices?.[0]?.delta;
|
||||
if (data.usage) usage = data.usage;
|
||||
if (delta?.content) {
|
||||
if (!ttftMs) ttftMs = Date.now() - startTime;
|
||||
accumulatedContent += delta.content;
|
||||
setStreamState({ content: accumulatedContent, phase: '' });
|
||||
|
||||
@@ -236,11 +238,23 @@ export function InputArea() {
|
||||
if (!accumulatedContent) {
|
||||
accumulatedContent = 'No response was generated. Please try again.';
|
||||
}
|
||||
const totalMs = Date.now() - startTime;
|
||||
const serverInfo = useAppStore.getState().serverInfo;
|
||||
const telemetry: MessageTelemetry = {
|
||||
engine: serverInfo?.engine,
|
||||
model_id: selectedModel,
|
||||
total_ms: totalMs,
|
||||
ttft_ms: ttftMs,
|
||||
tokens_per_sec: usage?.completion_tokens
|
||||
? usage.completion_tokens / (totalMs / 1000)
|
||||
: undefined,
|
||||
};
|
||||
updateLastAssistant(
|
||||
convId,
|
||||
accumulatedContent,
|
||||
toolCalls.length > 0 ? toolCalls : undefined,
|
||||
usage,
|
||||
telemetry,
|
||||
);
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
|
||||
@@ -7,6 +7,7 @@ import remarkMath from 'remark-math';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { Copy, Check } from 'lucide-react';
|
||||
import { ToolCallCard } from './ToolCallCard';
|
||||
import { XRayFooter } from './XRayFooter';
|
||||
import type { ChatMessage } from '../../types';
|
||||
|
||||
function stripThinkTags(text: string): string {
|
||||
@@ -145,15 +146,11 @@ export function MessageBubble({ message }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer: usage + copy */}
|
||||
<div className="flex items-center gap-2 mt-1.5 min-h-[24px]">
|
||||
{/* Footer: copy + x-ray */}
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
<CopyMessageButton content={cleanContent} />
|
||||
{message.usage && (
|
||||
<span className="text-[11px]" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{message.usage.total_tokens} tokens
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<XRayFooter usage={message.usage} telemetry={message.telemetry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { TokenUsage, MessageTelemetry } from '../../types';
|
||||
|
||||
interface Props {
|
||||
usage?: TokenUsage;
|
||||
telemetry?: MessageTelemetry;
|
||||
}
|
||||
|
||||
function formatMs(ms: number): string {
|
||||
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
export function XRayFooter({ usage, telemetry }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Build collapsed summary parts
|
||||
const parts: string[] = [];
|
||||
if (telemetry?.engine) parts.push(telemetry.engine);
|
||||
if (telemetry?.model_id) parts.push(telemetry.model_id);
|
||||
if (usage?.completion_tokens) parts.push(`${usage.completion_tokens} tok`);
|
||||
if (telemetry?.total_ms) parts.push(formatMs(telemetry.total_ms));
|
||||
|
||||
if (parts.length === 0 && !usage?.total_tokens) return null;
|
||||
|
||||
// Fallback: just show total tokens if no telemetry
|
||||
const summary = parts.length > 0 ? parts.join(' \u00B7 ') : `${usage!.total_tokens} tokens`;
|
||||
|
||||
// Build expanded rows
|
||||
const rows: Array<{ label: string; value: string; color?: string }> = [];
|
||||
if (telemetry?.engine) {
|
||||
const modelDetail = telemetry.model_id || '';
|
||||
rows.push({ label: 'Engine', value: `${telemetry.engine}${modelDetail ? ` (${modelDetail})` : ''}` });
|
||||
}
|
||||
if (usage) {
|
||||
rows.push({ label: 'Tokens', value: `${usage.completion_tokens} generated \u00B7 ${usage.prompt_tokens} prompt` });
|
||||
}
|
||||
if (telemetry?.tokens_per_sec) {
|
||||
rows.push({ label: 'Speed', value: `${Math.round(telemetry.tokens_per_sec)} tok/s` });
|
||||
}
|
||||
if (telemetry?.ttft_ms != null || telemetry?.total_ms != null) {
|
||||
const latencyParts: string[] = [];
|
||||
if (telemetry.ttft_ms != null) latencyParts.push(`TTFT ${formatMs(telemetry.ttft_ms)}`);
|
||||
if (telemetry.total_ms != null) latencyParts.push(`Total ${formatMs(telemetry.total_ms)}`);
|
||||
rows.push({ label: 'Latency', value: latencyParts.join(' \u00B7 ') });
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ borderTop: '1px solid var(--color-border-subtle)', marginTop: '0.375rem' }}>
|
||||
{/* Collapsed row */}
|
||||
<button
|
||||
onClick={() => rows.length > 0 && setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 w-full text-left py-1"
|
||||
style={{ cursor: rows.length > 0 ? 'pointer' : 'default' }}
|
||||
>
|
||||
<span
|
||||
className="w-1 h-1 rounded-full shrink-0"
|
||||
style={{ background: 'var(--color-accent)' }}
|
||||
/>
|
||||
<span
|
||||
className="text-[11px] flex-1"
|
||||
style={{ color: 'var(--color-text-tertiary)', fontFamily: 'system-ui' }}
|
||||
>
|
||||
{summary}
|
||||
</span>
|
||||
{rows.length > 0 && (
|
||||
expanded
|
||||
? <ChevronUp size={10} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
: <ChevronDown size={10} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expanded trace */}
|
||||
{expanded && rows.length > 0 && (
|
||||
<div
|
||||
className="rounded-lg mt-1 px-3 py-2"
|
||||
style={{ background: 'rgba(0, 0, 0, 0.15)' }}
|
||||
>
|
||||
<div className="grid gap-y-0.5" style={{ gridTemplateColumns: 'auto 1fr', columnGap: '1rem' }}>
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="contents">
|
||||
<span className="text-[11px]" style={{ color: 'var(--color-text-tertiary)', fontFamily: 'monospace' }}>
|
||||
{row.label}
|
||||
</span>
|
||||
<span
|
||||
className="text-[11px]"
|
||||
style={{ color: row.color || 'var(--color-text-secondary)', fontFamily: 'monospace' }}
|
||||
>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,69 @@
|
||||
import { Outlet } from 'react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router';
|
||||
import { Sidebar } from './Sidebar/Sidebar';
|
||||
import { SystemPulse } from './SystemPulse';
|
||||
import { useAppStore } from '../lib/store';
|
||||
import { checkHealth } from '../lib/api';
|
||||
|
||||
export function Layout() {
|
||||
const sidebarOpen = useAppStore((s) => s.sidebarOpen);
|
||||
const [apiReachable, setApiReachable] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => checkHealth().then(setApiReachable);
|
||||
check();
|
||||
const interval = setInterval(check, 30000);
|
||||
const onFocus = () => check();
|
||||
window.addEventListener('focus', onFocus);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.removeEventListener('focus', onFocus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full overflow-hidden">
|
||||
<Sidebar />
|
||||
{/* Overlay for mobile when sidebar is open */}
|
||||
{sidebarOpen && (
|
||||
<div className="flex flex-col h-full w-full overflow-hidden" style={{ paddingTop: '3px' }}>
|
||||
<SystemPulse apiReachable={apiReachable} />
|
||||
|
||||
{/* Health check banner */}
|
||||
{apiReachable === false && (
|
||||
<div
|
||||
className="fixed inset-0 z-20 bg-black/40 md:hidden"
|
||||
onClick={() => useAppStore.getState().setSidebarOpen(false)}
|
||||
/>
|
||||
className="flex items-center gap-3 px-4 py-2 text-sm shrink-0"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.08)',
|
||||
borderBottom: '1px solid rgba(239, 68, 68, 0.15)',
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||
style={{ background: 'var(--color-error)' }}
|
||||
/>
|
||||
<span>Cannot reach OpenJarvis backend</span>
|
||||
<button
|
||||
onClick={() => navigate('/settings')}
|
||||
className="text-sm underline cursor-pointer ml-auto shrink-0"
|
||||
style={{ color: 'var(--color-accent)' }}
|
||||
>
|
||||
Change URL
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<main className="flex-1 flex flex-col min-w-0 h-full" style={{ background: 'var(--color-bg)' }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<Sidebar />
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-20 bg-black/40 md:hidden"
|
||||
onClick={() => useAppStore.getState().setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<main className="flex-1 flex flex-col min-w-0 h-full" style={{ background: 'var(--color-bg)' }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,12 @@ export function Sidebar() {
|
||||
fixed md:relative z-30
|
||||
${sidebarOpen ? 'w-[260px]' : 'w-0'}
|
||||
`}
|
||||
style={{ background: 'var(--color-sidebar)', borderRight: sidebarOpen ? '1px solid var(--color-border)' : 'none' }}
|
||||
style={{
|
||||
background: 'var(--color-sidebar)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
borderRight: sidebarOpen ? '1px solid var(--color-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col h-full w-[260px]">
|
||||
{/* Header */}
|
||||
@@ -182,7 +187,7 @@ export function Sidebar() {
|
||||
onClick={() => navigate(item.path)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors w-full text-left cursor-pointer"
|
||||
style={{
|
||||
background: isActive ? 'var(--color-bg-tertiary)' : 'transparent',
|
||||
background: isActive ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
color: isActive ? 'var(--color-text)' : 'var(--color-text-secondary)',
|
||||
fontWeight: isActive ? 500 : 400,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppStore } from '../lib/store';
|
||||
import { fetchManagedAgents } from '../lib/api';
|
||||
|
||||
type PulseState = 'idle' | 'inferencing' | 'agent-active' | 'hidden';
|
||||
|
||||
const PULSE_CONFIG: Record<Exclude<PulseState, 'hidden'>, { color: string; animation: string }> = {
|
||||
idle: {
|
||||
color: 'rgba(59, 130, 246, 0.3)',
|
||||
animation: 'none',
|
||||
},
|
||||
inferencing: {
|
||||
color: '#3b82f6',
|
||||
animation: 'pulse-glow 2s ease-in-out infinite',
|
||||
},
|
||||
'agent-active': {
|
||||
color: '#8b5cf6',
|
||||
animation: 'pulse-travel 3s linear infinite',
|
||||
},
|
||||
};
|
||||
|
||||
export function SystemPulse({ apiReachable }: { apiReachable: boolean | null }) {
|
||||
const isStreaming = useAppStore((s) => s.streamState.isStreaming);
|
||||
const [hasRunningAgent, setHasRunningAgent] = useState(false);
|
||||
|
||||
// Poll for running agents every 30s
|
||||
useEffect(() => {
|
||||
if (apiReachable === false) return;
|
||||
const check = () =>
|
||||
fetchManagedAgents()
|
||||
.then((agents) => setHasRunningAgent(agents.some((a) => a.status === 'running')))
|
||||
.catch(() => {});
|
||||
check();
|
||||
const interval = setInterval(check, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [apiReachable]);
|
||||
|
||||
if (apiReachable === false) return null;
|
||||
|
||||
// Priority: agent-active > inferencing > idle
|
||||
let state: PulseState = 'idle';
|
||||
if (isStreaming) state = 'inferencing';
|
||||
if (hasRunningAgent) state = 'agent-active';
|
||||
|
||||
const config = PULSE_CONFIG[state];
|
||||
const isTravel = state === 'agent-active';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 h-[3px] z-50"
|
||||
style={{
|
||||
background: isTravel
|
||||
? `linear-gradient(90deg, transparent, ${config.color}, transparent)`
|
||||
: `linear-gradient(90deg, transparent 5%, ${config.color} 30%, ${config.color} 70%, transparent 95%)`,
|
||||
backgroundSize: isTravel ? '200% 100%' : '100% 100%',
|
||||
animation: config.animation,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-base leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,199 @@
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Trigger.Props & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon
|
||||
render={
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
alignItemWithTrigger = true,
|
||||
...props
|
||||
}: SelectPrimitive.Popup.Props &
|
||||
Pick<
|
||||
SelectPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||
>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
alignItemWithTrigger={alignItemWithTrigger}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Popup>
|
||||
</SelectPrimitive.Positioner>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<SelectPrimitive.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Item.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Separator.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollUpArrow>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollDownArrow>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
<CircleCheckIcon className="size-4" />
|
||||
),
|
||||
info: (
|
||||
<InfoIcon className="size-4" />
|
||||
),
|
||||
warning: (
|
||||
<TriangleAlertIcon className="size-4" />
|
||||
),
|
||||
error: (
|
||||
<OctagonXIcon className="size-4" />
|
||||
),
|
||||
loading: (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
),
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delay = 0,
|
||||
...props
|
||||
}: TooltipPrimitive.Provider.Props) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delay={delay}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<
|
||||
TooltipPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
+180
-37
@@ -1,4 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/*
|
||||
* OpenJarvis design tokens.
|
||||
@@ -7,23 +10,31 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--color-bg: #ffffff;
|
||||
--color-bg: #f9f9f9;
|
||||
--color-bg-secondary: #f4f4f5;
|
||||
--color-bg-tertiary: #e4e4e7;
|
||||
--color-surface: #ffffff;
|
||||
--color-sidebar: #fafafa;
|
||||
--color-border: #e4e4e7;
|
||||
--color-border-subtle: #f4f4f5;
|
||||
--color-sidebar: rgba(250, 250, 250, 0.85);
|
||||
--color-border: rgba(0, 0, 0, 0.08);
|
||||
--color-border-subtle: rgba(0, 0, 0, 0.04);
|
||||
|
||||
--color-text: #09090b;
|
||||
--color-text-secondary: #71717a;
|
||||
--color-text-tertiary: #a1a1aa;
|
||||
--color-text-inverse: #fafafa;
|
||||
|
||||
--color-accent: #2563eb;
|
||||
--color-accent-hover: #1d4ed8;
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #2563eb;
|
||||
--color-accent-subtle: #eff6ff;
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber-hover: #d97706;
|
||||
--color-accent-amber-subtle: rgba(245, 158, 11, 0.1);
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #7c3aed;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.1);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.1);
|
||||
|
||||
--color-success: #16a34a;
|
||||
--color-warning: #ca8a04;
|
||||
--color-error: #dc2626;
|
||||
@@ -32,7 +43,7 @@
|
||||
--color-input-bg: #ffffff;
|
||||
--color-input-border: #d4d4d8;
|
||||
|
||||
--color-user-bubble: #2563eb;
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
@@ -52,30 +63,38 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg: #09090b;
|
||||
--color-bg-secondary: #18181b;
|
||||
--color-bg-tertiary: #27272a;
|
||||
--color-surface: #18181b;
|
||||
--color-sidebar: #0f0f12;
|
||||
--color-border: #27272a;
|
||||
--color-border-subtle: #1e1e22;
|
||||
--color-bg: #161618;
|
||||
--color-bg-secondary: #1e1e21;
|
||||
--color-bg-tertiary: #2a2a2d;
|
||||
--color-surface: #1e1e21;
|
||||
--color-sidebar: rgba(30, 30, 33, 0.85);
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.04);
|
||||
|
||||
--color-text: #fafafa;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-tertiary: #71717a;
|
||||
--color-text-inverse: #09090b;
|
||||
--color-text: #e8e8ed;
|
||||
--color-text-secondary: #8e8e93;
|
||||
--color-text-tertiary: #636366;
|
||||
--color-text-inverse: #161618;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #60a5fa;
|
||||
--color-accent-subtle: #172554;
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber-hover: #fbbf24;
|
||||
--color-accent-amber-subtle: rgba(245, 158, 11, 0.15);
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #a78bfa;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.15);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.15);
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #eab308;
|
||||
--color-error: #ef4444;
|
||||
|
||||
--color-code-bg: #1e1e22;
|
||||
--color-input-bg: #18181b;
|
||||
--color-input-border: #3f3f46;
|
||||
--color-input-bg: #1e1e21;
|
||||
--color-input-border: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
@@ -89,30 +108,38 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) {
|
||||
--color-bg: #09090b;
|
||||
--color-bg-secondary: #18181b;
|
||||
--color-bg-tertiary: #27272a;
|
||||
--color-surface: #18181b;
|
||||
--color-sidebar: #0f0f12;
|
||||
--color-border: #27272a;
|
||||
--color-border-subtle: #1e1e22;
|
||||
--color-bg: #161618;
|
||||
--color-bg-secondary: #1e1e21;
|
||||
--color-bg-tertiary: #2a2a2d;
|
||||
--color-surface: #1e1e21;
|
||||
--color-sidebar: rgba(30, 30, 33, 0.85);
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.04);
|
||||
|
||||
--color-text: #fafafa;
|
||||
--color-text-secondary: #a1a1aa;
|
||||
--color-text-tertiary: #71717a;
|
||||
--color-text-inverse: #09090b;
|
||||
--color-text: #e8e8ed;
|
||||
--color-text-secondary: #8e8e93;
|
||||
--color-text-tertiary: #636366;
|
||||
--color-text-inverse: #161618;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #60a5fa;
|
||||
--color-accent-subtle: #172554;
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber-hover: #fbbf24;
|
||||
--color-accent-amber-subtle: rgba(245, 158, 11, 0.15);
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #a78bfa;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.15);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.15);
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #eab308;
|
||||
--color-error: #ef4444;
|
||||
|
||||
--color-code-bg: #1e1e22;
|
||||
--color-input-bg: #18181b;
|
||||
--color-input-border: #3f3f46;
|
||||
--color-input-bg: #1e1e21;
|
||||
--color-input-border: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
@@ -138,9 +165,10 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Merriweather", Georgia, "Times New Roman", serif;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.7;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.01em;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -241,8 +269,8 @@
|
||||
|
||||
/* Markdown prose styling */
|
||||
.prose {
|
||||
line-height: 1.75;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.55;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
@@ -298,6 +326,7 @@
|
||||
font-weight: 600;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.prose h1 { font-size: 1.375em; }
|
||||
@@ -337,4 +366,118 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* System Pulse animations */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse-travel {
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--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.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--radius: 0.625rem;
|
||||
--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);
|
||||
}
|
||||
.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.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--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);
|
||||
}
|
||||
@theme inline {
|
||||
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
/* shadcn component tokens — namespaced to avoid collision with OpenJarvis design tokens */
|
||||
--color-cn-sidebar-ring: var(--sidebar-ring);
|
||||
--color-cn-sidebar-border: var(--sidebar-border);
|
||||
--color-cn-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-cn-sidebar-accent: var(--sidebar-accent);
|
||||
--color-cn-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-cn-sidebar-primary: var(--sidebar-primary);
|
||||
--color-cn-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-cn-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-cn-input: var(--input);
|
||||
--color-cn-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-cn-accent-foreground: var(--accent-foreground);
|
||||
--color-cn-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
/* Keep project radius tokens — don't override with shadcn calc values */
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ChatMessage,
|
||||
LogEntry,
|
||||
ModelInfo,
|
||||
MessageTelemetry,
|
||||
SavingsData,
|
||||
ServerInfo,
|
||||
StreamState,
|
||||
@@ -147,6 +148,7 @@ interface AppState {
|
||||
content: string,
|
||||
toolCalls?: ToolCallInfo[],
|
||||
usage?: TokenUsage,
|
||||
telemetry?: MessageTelemetry,
|
||||
) => void;
|
||||
setStreamState: (state: Partial<StreamState>) => void;
|
||||
resetStream: () => void;
|
||||
@@ -334,6 +336,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
content: string,
|
||||
toolCalls?: ToolCallInfo[],
|
||||
usage?: TokenUsage,
|
||||
telemetry?: MessageTelemetry,
|
||||
) => {
|
||||
const store = loadConversations();
|
||||
const conv = store.conversations[conversationId];
|
||||
@@ -343,6 +346,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
lastMsg.content = content;
|
||||
if (toolCalls) lastMsg.toolCalls = toolCalls;
|
||||
if (usage) lastMsg.usage = usage;
|
||||
if (telemetry) lastMsg.telemetry = telemetry;
|
||||
conv.updatedAt = Date.now();
|
||||
saveConversations(store);
|
||||
set({ messages: [...conv.messages] });
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useAppStore } from '../lib/store';
|
||||
import {
|
||||
fetchManagedAgents,
|
||||
@@ -161,7 +162,6 @@ function LaunchWizard({
|
||||
learningEnabled: false,
|
||||
});
|
||||
const [launching, setLaunching] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function update(partial: Partial<WizardState>) {
|
||||
setWizard((prev) => ({ ...prev, ...partial }));
|
||||
@@ -184,11 +184,10 @@ function LaunchWizard({
|
||||
|
||||
async function handleLaunch() {
|
||||
if (!wizard.name.trim()) {
|
||||
setError('Agent name is required.');
|
||||
toast.error('Agent name is required');
|
||||
return;
|
||||
}
|
||||
setLaunching(true);
|
||||
setError('');
|
||||
try {
|
||||
const config: Record<string, unknown> = {
|
||||
schedule_type: wizard.scheduleType,
|
||||
@@ -202,9 +201,12 @@ function LaunchWizard({
|
||||
template_id: wizard.templateId || undefined,
|
||||
config,
|
||||
});
|
||||
toast.success(`Agent "${wizard.name}" launched`);
|
||||
onLaunched();
|
||||
} catch {
|
||||
setError('Failed to create agent. Please try again.');
|
||||
} catch (err) {
|
||||
toast.error('Could not create agent', {
|
||||
description: 'Agent manager endpoint not available. Check that agent_manager.enabled = true in your config.',
|
||||
});
|
||||
} finally {
|
||||
setLaunching(false);
|
||||
}
|
||||
@@ -466,11 +468,6 @@ function LaunchWizard({
|
||||
<span style={{ color: 'var(--color-text)' }}>{wizard.learningEnabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm" style={{ color: '#ef4444' }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -982,6 +979,7 @@ export function AgentsPage() {
|
||||
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
|
||||
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [agentManagerAvailable, setAgentManagerAvailable] = useState<boolean | null>(null);
|
||||
const [tasks, setTasks] = useState<AgentTask[]>([]);
|
||||
const [channels, setChannels] = useState<ChannelBinding[]>([]);
|
||||
const [templates, setTemplates] = useState<AgentTemplate[]>([]);
|
||||
@@ -992,8 +990,12 @@ export function AgentsPage() {
|
||||
try {
|
||||
const agents = await fetchManagedAgents();
|
||||
setManagedAgents(agents);
|
||||
} catch {
|
||||
// Server may be down
|
||||
setAgentManagerAvailable(true);
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes('404')) {
|
||||
setAgentManagerAvailable(false);
|
||||
}
|
||||
setManagedAgents([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -1326,14 +1328,32 @@ export function AgentsPage() {
|
||||
Agents
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowWizard(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer transition-colors"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff' }}
|
||||
onClick={() => agentManagerAvailable && setShowWizard(true)}
|
||||
disabled={agentManagerAvailable === false}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: agentManagerAvailable === false ? 'var(--color-bg-tertiary)' : 'var(--color-accent)',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : '#fff',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> New Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{agentManagerAvailable === false && (
|
||||
<div
|
||||
className="mx-4 mt-2 px-4 py-3 rounded-lg flex items-center gap-3 text-sm"
|
||||
style={{
|
||||
background: 'var(--color-accent-amber-subtle)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.2)',
|
||||
color: 'var(--color-accent-amber)',
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
<span>Agent manager is not enabled. Set <code className="font-mono text-xs">agent_manager.enabled = true</code> in your config.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent cards grid */}
|
||||
<div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>
|
||||
{managedAgents.map((a) => (
|
||||
@@ -1361,9 +1381,13 @@ export function AgentsPage() {
|
||||
</p>
|
||||
<p className="text-sm mb-6">Create your first agent to get started with autonomous task management.</p>
|
||||
<button
|
||||
onClick={() => setShowWizard(true)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff' }}
|
||||
onClick={() => agentManagerAvailable && setShowWizard(true)}
|
||||
disabled={agentManagerAvailable === false}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: agentManagerAvailable === false ? 'var(--color-bg-tertiary)' : 'var(--color-accent)',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : '#fff',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> Launch your first agent
|
||||
</button>
|
||||
|
||||
@@ -50,6 +50,14 @@ export interface TokenUsage {
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export interface MessageTelemetry {
|
||||
engine?: string;
|
||||
model_id?: string;
|
||||
tokens_per_sec?: number;
|
||||
ttft_ms?: number;
|
||||
total_ms?: number;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
@@ -57,6 +65,7 @@ export interface ChatMessage {
|
||||
timestamp: number;
|
||||
toolCalls?: ToolCallInfo[];
|
||||
usage?: TokenUsage;
|
||||
telemetry?: MessageTelemetry;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
@@ -13,8 +19,8 @@ export default defineConfig({
|
||||
name: 'OpenJarvis',
|
||||
short_name: 'Jarvis',
|
||||
description: 'On-device AI assistant',
|
||||
theme_color: '#09090b',
|
||||
background_color: '#09090b',
|
||||
theme_color: '#161618',
|
||||
background_color: '#161618',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||
|
||||
Reference in New Issue
Block a user