feat(themes): Added custom theme support.

This commit is contained in:
Willie Zutz 2025-08-09 17:30:12 -06:00
parent 58a3f8efbc
commit 2222928623
48 changed files with 2273 additions and 1590 deletions

View file

@ -151,3 +151,4 @@ When working on this codebase, you might need to:
- `/langchain-ai/langgraph` for LangGraph
- `/quantizor/markdown-to-jsx` for Markdown to JSX conversion
- `/context7/headlessui_com` for Headless UI components
- `/tailwindlabs/tailwindcss.com` for Tailwind CSS documentation

1300
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,6 +61,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/better-sqlite3": "^7.6.12",
"@types/html-to-text": "^9.0.4",
"@types/jsdom": "^21.1.7",
@ -77,7 +78,7 @@
"eslint-config-next": "14.1.4",
"postcss": "^8",
"prettier": "^3.2.5",
"tailwindcss": "^3.3.0",
"tailwindcss": "^4.0.0",
"typescript": "^5"
}
}

View file

@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

View file

@ -159,7 +159,7 @@ const DashboardPage = () => {
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<p className="text-sm text-fg/60 mb-4">
Widgets let you fetch content from any URL and process it with AI to
show exactly what you need.
</p>
@ -168,7 +168,7 @@ const DashboardPage = () => {
<CardFooter className="justify-center">
<button
onClick={handleAddWidget}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition duration-200 flex items-center space-x-2"
className="px-4 py-2 bg-accent text-white rounded hover:bg-accent-700 transition duration-200 flex items-center space-x-2"
>
<Plus size={16} />
<span>Create Your First Widget</span>
@ -191,50 +191,50 @@ const DashboardPage = () => {
<div className="flex items-center space-x-2">
<button
onClick={handleRefreshAll}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
className="p-2 hover:bg-surface-2 rounded-lg transition duration-200"
title="Refresh All Widgets"
>
<RefreshCw size={18} className="text-black dark:text-white" />
<RefreshCw size={18} />
</button>
<button
onClick={handleToggleProcessingMode}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
className="p-2 hover:bg-surface-2 rounded-lg transition duration-200"
title={`Switch to ${settings.parallelLoading ? 'Sequential' : 'Parallel'} Processing`}
>
{settings.parallelLoading ? (
<Layers size={18} className="text-black dark:text-white" />
<Layers size={18} />
) : (
<List size={18} className="text-black dark:text-white" />
<List size={18} />
)}
</button>
<button
onClick={handleExport}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
className="p-2 hover:bg-surface-2 rounded-lg transition duration-200"
title="Export Dashboard Configuration"
>
<Download size={18} className="text-black dark:text-white" />
<Download size={18} />
</button>
<button
onClick={handleImport}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
className="p-2 hover:bg-surface-2 rounded-lg transition duration-200"
title="Import Dashboard Configuration"
>
<Upload size={18} className="text-black dark:text-white" />
<Upload size={18} />
</button>
<button
onClick={handleAddWidget}
className="p-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition duration-200"
className="p-2 bg-accent hover:bg-accent-700 rounded-lg transition duration-200"
title="Add New Widget"
>
<Plus size={18} className="text-white" />
<Plus size={18} />
</button>
</div>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
<hr className="border-t my-4 w-full" />
</div>
{/* Main content area */}
@ -242,10 +242,8 @@ const DashboardPage = () => {
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">
Loading dashboard...
</p>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 mx-auto mb-4"></div>
<p className="text-fg/60">Loading dashboard...</p>
</div>
</div>
) : widgets.length === 0 ? (

View file

@ -50,7 +50,7 @@ const Page = () => {
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
className="w-8 h-8 text-fg/20 fill-fg/30 animate-spin"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@ -73,7 +73,7 @@ const Page = () => {
<Search />
<h1 className="text-3xl font-medium p-2">Discover</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
<hr className="border-t border-surface-2 my-4 w-full" />
</div>
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
@ -82,7 +82,7 @@ const Page = () => {
<Link
href={`/?q=Summary: ${item.url}`}
key={i}
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
className="max-w-sm rounded-lg overflow-hidden bg-surface border border-surface-2 hover:-translate-y-[1px] transition duration-200"
target="_blank"
>
<img
@ -95,10 +95,10 @@ const Page = () => {
alt={item.title}
/>
<div className="px-6 py-4">
<div className="font-bold text-lg mb-2">
<div className="font-bold text-lg mb-2 text-fg">
{item.title.slice(0, 100)}...
</div>
<p className="text-black-70 dark:text-white/70 text-sm">
<p className="text-fg/70 text-sm">
{item.content.slice(0, 100)}...
</p>
</div>

View file

@ -2,9 +2,36 @@
@import 'react-grid-layout/css/styles.css';
@import 'react-resizable/css/styles.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
/* Theme tokens */
@theme {
/* Base palette (light by default) */
--color-bg: oklch(0.98 0 0); /* canvas/background */
--color-fg: oklch(0.21 0 0); /* text */
--color-accent-600: var(--color-blue-600);
--color-accent-700: var(--color-blue-700);
--color-accent-500: var(--color-blue-500);
--color-surface: color-mix(in oklch, var(--color-bg), black 6%);
--color-surface-2: color-mix(in oklch, var(--color-bg), black 10%);
/* Shorthands that Tailwind maps into utilities */
--color-accent: var(--color-accent-600);
}
:root {
color-scheme: light;
}
[data-theme='dark'] {
color-scheme: dark;
--color-bg: oklch(0.16 0 0);
--color-fg: oklch(0.95 0 0);
--color-surface: color-mix(in oklch, var(--color-bg), white 8%);
--color-surface-2: color-mix(in oklch, var(--color-bg), white 12%);
}
/* Custom theme overrides are applied via CSS variables on :root by ThemeController */
@layer base {
.overflow-hidden-scrollable {
@ -14,6 +41,39 @@
.overflow-hidden-scrollable::-webkit-scrollbar {
display: none;
}
html,
body {
min-height: 100dvh;
}
button:not(:disabled),
[role='button']:not(:disabled),
input[type='button']:not(:disabled),
input[type='submit']:not(:disabled),
input[type='reset']:not(:disabled) {
cursor: pointer;
color: var(--color-fg);
}
button:not(:disabled):hover,
[role='button']:not(:disabled):hover,
input[type='button']:not(:disabled):hover,
input[type='submit']:not(:disabled):hover,
input[type='reset']:not(:disabled):hover {
color: var(--color-accent);
}
input[type='text']:focus,
textarea:focus {
border-color: var(--color-accent);
outline: none;
border: 1px solid var(--color-accent);
}
a:hover {
color: var(--color-accent);
}
}
@layer utilities {
@ -37,3 +97,55 @@
font-size: 16px !important;
}
}
/* Utilities are auto-generated from @theme tokens */
@layer utilities {
/* Backwards-compat for prior custom palette names */
.bg-light-primary {
background-color: var(--color-bg);
}
.dark .bg-dark-primary {
background-color: var(--color-bg);
}
.bg-light-secondary {
background-color: var(--color-surface);
}
.dark .bg-dark-secondary {
background-color: var(--color-surface);
}
.bg-light-100 {
background-color: var(--color-surface-2);
}
.dark .bg-dark-100 {
background-color: var(--color-surface-2);
}
.hover\:bg-light-200:hover {
background-color: var(--color-surface-2);
}
.dark .hover\:bg-dark-200:hover {
background-color: var(--color-surface-2);
}
.border-light-200 {
border-color: var(--color-surface-2);
}
.dark .border-dark-200 {
border-color: var(--color-surface-2);
}
/* Preferred token utilities */
.bg-bg {
background-color: var(--color-bg);
}
.bg-surface {
background-color: var(--color-surface);
}
.bg-surface-2 {
background-color: var(--color-surface-2);
}
.text-fg {
color: var(--color-fg);
}
.border-surface-2 {
border-color: var(--color-surface-2);
}
}

View file

@ -4,7 +4,7 @@ import './globals.css';
import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider';
import ThemeController from '@/components/theme/Controller';
const montserrat = Montserrat({
weight: ['300', '400', '500', '700'],
@ -25,7 +25,12 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html className="h-full" lang="en" suppressHydrationWarning>
<html
className="h-full dark"
lang="en"
suppressHydrationWarning
data-theme="dark"
>
<head>
<link
rel="search"
@ -34,19 +39,19 @@ export default function RootLayout({
href="/api/opensearch"
/>
</head>
<body className={cn('h-full', montserrat.className)}>
<ThemeProvider>
<body className={cn('h-full bg-bg text-fg', montserrat.className)}>
<ThemeController>
<Sidebar>{children}</Sidebar>
<Toaster
toastOptions={{
unstyled: true,
classNames: {
toast:
'bg-light-primary dark:bg-dark-secondary dark:text-white/70 text-black-70 rounded-lg p-4 flex flex-row items-center space-x-2',
'bg-surface text-fg rounded-lg p-4 flex flex-row items-center space-x-2',
},
}}
/>
</ThemeProvider>
</ThemeController>
</body>
</html>
);

View file

@ -41,7 +41,7 @@ const Page = () => {
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
className="w-8 h-8 text-fg/20 fill-fg/30 animate-spin"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@ -63,13 +63,11 @@ const Page = () => {
<BookOpenText />
<h1 className="text-3xl font-medium p-2">Library</h1>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
<hr className="border-t border-surface-2 my-4 w-full" />
</div>
{chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen">
<p className="text-black/70 dark:text-white/70 text-sm">
No chats found.
</p>
<p className="text-fg/70 text-sm">No chats found.</p>
</div>
)}
{chats.length > 0 && (
@ -78,20 +76,18 @@ const Page = () => {
<div
className={cn(
'flex flex-col space-y-4 py-6',
i !== chats.length - 1
? 'border-b border-white-200 dark:border-dark-200'
: '',
i !== chats.length - 1 ? 'border-b border-surface-2' : '',
)}
key={i}
>
<Link
href={`/c/${chat.id}`}
className="text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer"
className="lg:text-xl font-medium truncate transition duration-200 cursor-pointer"
>
{chat.title}
</Link>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 opacity-70">
<ClockIcon size={15} />
<p className="text-xs">
{formatTimeDifference(new Date(), chat.createdAt)} Ago

View file

@ -65,7 +65,7 @@ const InputComponent = ({
<input
{...restProps}
className={cn(
'bg-light-secondary dark:bg-dark-secondary w-full px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
'bg-surface w-full px-3 py-2 flex items-center overflow-hidden rounded-lg text-sm',
isSaving && 'pr-10',
className,
)}
@ -73,10 +73,7 @@ const InputComponent = ({
/>
{isSaving && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2
size={16}
className="animate-spin text-black/70 dark:text-white/70"
/>
<Loader2 size={16} className="animate-spin" />
</div>
)}
</div>
@ -98,17 +95,14 @@ const TextareaComponent = ({
<div className="relative">
<textarea
placeholder="Any special instructions for the LLM"
className="placeholder:text-sm text-sm w-full flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors"
className="placeholder:text-sm text-sm w-full flex items-center justify-between p-3 bg-surface rounded-lg hover:bg-surface-2 transition-colors"
rows={4}
onBlur={(e) => onSave?.(e.target.value)}
{...restProps}
/>
{isSaving && (
<div className="absolute right-3 top-3">
<Loader2
size={16}
className="animate-spin text-black/70 dark:text-white/70"
/>
<Loader2 size={16} className="animate-spin" />
</div>
)}
</div>
@ -126,7 +120,7 @@ const Select = ({
<select
{...restProps}
className={cn(
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
'bg-surface px-3 py-2 flex items-center overflow-hidden border border-surface-2 rounded-lg text-sm',
className,
)}
>
@ -171,16 +165,14 @@ const SettingsSection = ({
}, []);
return (
<div className="flex flex-col space-y-4 p-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200">
<div className="flex flex-col space-y-4 p-4 bg-surface rounded-xl border border-surface-2">
<div className="flex items-center gap-2">
<h2 className="text-black/90 dark:text-white/90 font-medium">
{title}
</h2>
<h2 className="font-medium">{title}</h2>
{tooltip && (
<div className="relative">
<button
ref={buttonRef}
className="p-1 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
className="p-1 rounded-full hover:bg-surface-2 transition duration-200"
onClick={() => setShowTooltip(!showTooltip)}
aria-label="Show section information"
>
@ -189,10 +181,10 @@ const SettingsSection = ({
{showTooltip && (
<div
ref={tooltipRef}
className="absolute z-10 left-6 top-0 w-96 rounded-md shadow-lg bg-white dark:bg-dark-secondary border border-light-200 dark:border-dark-200"
className="absolute z-10 left-6 top-0 w-96 rounded-md shadow-lg bg-surface border border-surface-2"
>
<div className="py-2 px-3">
<div className="space-y-1 text-xs text-black dark:text-white">
<div className="space-y-1 text-xs">
{tooltip.split('\\n').map((line, index) => (
<div key={index}>{line}</div>
))}
@ -733,21 +725,21 @@ export default function SettingsPage() {
<div className="flex flex-col pt-4">
<div className="flex items-center space-x-2">
<Link href="/" className="lg:hidden">
<ArrowLeft className="text-black/70 dark:text-white/70" />
<ArrowLeft />
</Link>
<div className="flex flex-row space-x-0.5 items-center">
<SettingsIcon size={23} />
<h1 className="text-3xl font-medium p-2">Settings</h1>
</div>
</div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
<hr className="border-t border-surface-2 my-4 w-full" />
</div>
{isLoading ? (
<div className="flex flex-row items-center justify-center min-h-[50vh]">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
className="w-8 h-8 text-surface-2 fill-surface animate-spin"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@ -767,15 +759,11 @@ export default function SettingsPage() {
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
<SettingsSection title="Preferences">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Theme
</p>
<p className="text-sm">Theme</p>
<ThemeSwitcher />
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Measurement Units
</p>
<p className="text-sm">Measurement Units</p>
<Select
value={measureUnit ?? undefined}
onChange={(e) => {
@ -798,19 +786,16 @@ export default function SettingsPage() {
<SettingsSection title="Automatic Search">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center justify-between p-3 bg-surface rounded-lg hover:bg-surface-2 transition-colors">
<div className="flex items-center space-x-3">
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
<Layers3
size={18}
className="text-black/70 dark:text-white/70"
/>
<div className="p-2 bg-surface-2 rounded-lg">
<Layers3 size={18} />
</div>
<div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
<p className="text-sm font-medium">
Automatic Suggestions
</p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
<p className="text-xs mt-0.5">
Automatically show related suggestions after responses
</p>
</div>
@ -822,9 +807,7 @@ export default function SettingsPage() {
saveConfig('automaticSuggestions', checked);
}}
className={cn(
automaticSuggestions
? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200',
automaticSuggestions ? 'bg-accent' : 'bg-surface-2',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
)}
>
@ -852,7 +835,7 @@ export default function SettingsPage() {
.map((prompt) => (
<div
key={prompt.id}
className="p-3 border border-light-secondary dark:border-dark-secondary rounded-md bg-light-100 dark:bg-dark-100"
className="p-3 border border-surface-2 rounded-md bg-surface-2"
>
{editingPrompt && editingPrompt.id === prompt.id ? (
<div className="space-y-3">
@ -868,7 +851,6 @@ export default function SettingsPage() {
})
}
placeholder="Prompt Name"
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
/>
<Select
value={editingPrompt.type}
@ -882,7 +864,6 @@ export default function SettingsPage() {
{ value: 'system', label: 'System Prompt' },
{ value: 'persona', label: 'Persona Prompt' },
]}
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
/>
<TextareaComponent
value={editingPrompt.content}
@ -895,19 +876,19 @@ export default function SettingsPage() {
})
}
placeholder="Prompt Content"
className="min-h-[100px] text-black dark:text-white bg-white dark:bg-dark-secondary"
className="min-h-[100px]"
/>
<div className="flex space-x-2 justify-end">
<button
onClick={() => setEditingPrompt(null)}
className="px-3 py-2 text-sm rounded-md bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-surface hover:bg-surface-2 flex items-center gap-1.5"
>
<X size={16} />
Cancel
</button>
<button
onClick={handleAddOrUpdateSystemPrompt}
className="px-3 py-2 text-sm rounded-md bg-[#24A0ED] hover:bg-[#1f8cdb] text-white flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md flex items-center gap-1.5 bg-accent"
>
<Save size={16} />
Save
@ -917,11 +898,9 @@ export default function SettingsPage() {
) : (
<div className="flex justify-between items-start">
<div className="flex-grow">
<h4 className="font-semibold text-black/90 dark:text-white/90">
{prompt.name}
</h4>
<h4 className="font-semibold">{prompt.name}</h4>
<p
className="text-sm text-black/70 dark:text-white/70 mt-1 whitespace-pre-wrap overflow-hidden text-ellipsis"
className="text-sm mt-1 whitespace-pre-wrap overflow-hidden text-ellipsis"
style={{
maxHeight: '3.6em',
display: '-webkit-box',
@ -936,7 +915,7 @@ export default function SettingsPage() {
<button
onClick={() => setEditingPrompt({ ...prompt })}
title="Edit"
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70"
className="p-1.5 rounded-md hover:bg-surface-2"
>
<Edit3 size={18} />
</button>
@ -945,7 +924,7 @@ export default function SettingsPage() {
handleDeleteSystemPrompt(prompt.id)
}
title="Delete"
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-500"
className="p-1.5 rounded-md hover:bg-surface-2 text-red-500 hover:text-red-600"
>
<Trash2 size={18} />
</button>
@ -955,7 +934,7 @@ export default function SettingsPage() {
</div>
))}
{isAddingNewPrompt && newPromptType === 'system' && (
<div className="p-3 border border-dashed border-light-secondary dark:border-dark-secondary rounded-md space-y-3 bg-light-100 dark:bg-dark-100">
<div className="p-3 border border-dashed border-surface-2 rounded-md space-y-3 bg-surface-2">
<InputComponent
type="text"
value={newPromptName}
@ -963,7 +942,6 @@ export default function SettingsPage() {
setNewPromptName(e.target.value)
}
placeholder="System Prompt Name"
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
/>
<TextareaComponent
value={newPromptContent}
@ -971,7 +949,7 @@ export default function SettingsPage() {
setNewPromptContent(e.target.value)
}
placeholder="System prompt content (e.g., '/nothink')"
className="min-h-[100px] text-black dark:text-white bg-white dark:bg-dark-secondary"
className="min-h-[100px]"
/>
<div className="flex space-x-2 justify-end">
<button
@ -981,14 +959,14 @@ export default function SettingsPage() {
setNewPromptContent('');
setNewPromptType('system');
}}
className="px-3 py-2 text-sm rounded-md bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-surface hover:bg-surface-2 flex items-center gap-1.5"
>
<X size={16} />
Cancel
</button>
<button
onClick={handleAddOrUpdateSystemPrompt}
className="px-3 py-2 text-sm rounded-md bg-[#24A0ED] hover:bg-[#1f8cdb] text-white flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md flex items-center gap-1.5 bg-accent"
>
<Save size={16} />
Add System Prompt
@ -1002,7 +980,7 @@ export default function SettingsPage() {
setIsAddingNewPrompt(true);
setNewPromptType('system');
}}
className="self-start px-3 py-2 text-sm rounded-md border border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="self-start px-3 py-2 text-sm rounded-md border border-surface-2 hover:bg-surface-2 flex items-center gap-1.5"
>
<PlusCircle size={18} /> Add System Prompt
</button>
@ -1020,7 +998,7 @@ export default function SettingsPage() {
.map((prompt) => (
<div
key={prompt.id}
className="p-3 border border-light-secondary dark:border-dark-secondary rounded-md bg-light-100 dark:bg-dark-100"
className="p-3 border border-surface-2 rounded-md bg-surface-2"
>
{editingPrompt && editingPrompt.id === prompt.id ? (
<div className="space-y-3">
@ -1036,7 +1014,7 @@ export default function SettingsPage() {
})
}
placeholder="Prompt Name"
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
className=""
/>
<Select
value={editingPrompt.type}
@ -1050,7 +1028,7 @@ export default function SettingsPage() {
{ value: 'system', label: 'System Prompt' },
{ value: 'persona', label: 'Persona Prompt' },
]}
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
className=""
/>
<TextareaComponent
value={editingPrompt.content}
@ -1063,19 +1041,19 @@ export default function SettingsPage() {
})
}
placeholder="Prompt Content"
className="min-h-[100px] text-black dark:text-white bg-white dark:bg-dark-secondary"
className="min-h-[100px]"
/>
<div className="flex space-x-2 justify-end">
<button
onClick={() => setEditingPrompt(null)}
className="px-3 py-2 text-sm rounded-md bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-surface hover:bg-surface-2 flex items-center gap-1.5"
>
<X size={16} />
Cancel
</button>
<button
onClick={handleAddOrUpdateSystemPrompt}
className="px-3 py-2 text-sm rounded-md bg-[#24A0ED] hover:bg-[#1f8cdb] text-white flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-accent flex items-center gap-1.5"
>
<Save size={16} />
Save
@ -1085,11 +1063,9 @@ export default function SettingsPage() {
) : (
<div className="flex justify-between items-start">
<div className="flex-grow">
<h4 className="font-semibold text-black/90 dark:text-white/90">
{prompt.name}
</h4>
<h4 className="font-semibold">{prompt.name}</h4>
<p
className="text-sm text-black/70 dark:text-white/70 mt-1 whitespace-pre-wrap overflow-hidden text-ellipsis"
className="text-sm mt-1 whitespace-pre-wrap overflow-hidden text-ellipsis"
style={{
maxHeight: '3.6em',
display: '-webkit-box',
@ -1104,7 +1080,7 @@ export default function SettingsPage() {
<button
onClick={() => setEditingPrompt({ ...prompt })}
title="Edit"
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70"
className="p-1.5 rounded-md hover:bg-surface-2"
>
<Edit3 size={18} />
</button>
@ -1113,7 +1089,7 @@ export default function SettingsPage() {
handleDeleteSystemPrompt(prompt.id)
}
title="Delete"
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-500"
className="p-1.5 rounded-md hover:bg-surface-2 text-red-500 hover:text-red-600"
>
<Trash2 size={18} />
</button>
@ -1123,7 +1099,7 @@ export default function SettingsPage() {
</div>
))}
{isAddingNewPrompt && newPromptType === 'persona' && (
<div className="p-3 border border-dashed border-light-secondary dark:border-dark-secondary rounded-md space-y-3 bg-light-100 dark:bg-dark-100">
<div className="p-3 border border-dashed border-surface-2 rounded-md space-y-3 bg-surface-2">
<InputComponent
type="text"
value={newPromptName}
@ -1131,7 +1107,7 @@ export default function SettingsPage() {
setNewPromptName(e.target.value)
}
placeholder="Persona Prompt Name"
className="text-black dark:text-white bg-white dark:bg-dark-secondary"
className=""
/>
<TextareaComponent
value={newPromptContent}
@ -1139,7 +1115,7 @@ export default function SettingsPage() {
setNewPromptContent(e.target.value)
}
placeholder="Persona prompt content (e.g., You are a helpful assistant that speaks like a pirate and uses nautical metaphors.)"
className="min-h-[100px] text-black dark:text-white bg-white dark:bg-dark-secondary"
className="min-h-[100px]"
/>
<div className="flex space-x-2 justify-end">
<button
@ -1149,14 +1125,14 @@ export default function SettingsPage() {
setNewPromptContent('');
setNewPromptType('system');
}}
className="px-3 py-2 text-sm rounded-md bg-light-secondary hover:bg-light-200 dark:bg-dark-secondary dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-surface hover:bg-surface-2 flex items-center gap-1.5"
>
<X size={16} />
Cancel
</button>
<button
onClick={handleAddOrUpdateSystemPrompt}
className="px-3 py-2 text-sm rounded-md bg-[#24A0ED] hover:bg-[#1f8cdb] text-white flex items-center gap-1.5"
className="px-3 py-2 text-sm rounded-md bg-accent flex items-center gap-1.5"
>
<Save size={16} />
Add Persona Prompt
@ -1170,7 +1146,7 @@ export default function SettingsPage() {
setIsAddingNewPrompt(true);
setNewPromptType('persona');
}}
className="self-start px-3 py-2 text-sm rounded-md border border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 text-black/80 dark:text-white/80 flex items-center gap-1.5"
className="self-start px-3 py-2 text-sm rounded-md border border-surface-2 hover:bg-surface-2 flex items-center gap-1.5"
>
<PlusCircle size={18} /> Add Persona Prompt
</button>
@ -1184,9 +1160,7 @@ export default function SettingsPage() {
>
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Optimization Mode
</p>
<p className="text-sm">Optimization Mode</p>
<div className="flex justify-start items-center space-x-2">
<Optimization
optimizationMode={searchOptimizationMode}
@ -1202,7 +1176,7 @@ export default function SettingsPage() {
setSearchOptimizationMode('');
localStorage.removeItem('searchOptimizationMode');
}}
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-black/50 dark:text-white/50 hover:text-black/80 dark:hover:text-white/80 transition-colors"
className="p-1.5 rounded-md hover:bg-surface-2 transition-colors"
title="Reset optimization mode"
>
<RotateCcw size={16} />
@ -1212,9 +1186,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Model
</p>
<p className="text-sm">Chat Model</p>
<div className="flex justify-start items-center space-x-2">
<ModelSelector
selectedModel={{
@ -1240,7 +1212,7 @@ export default function SettingsPage() {
localStorage.removeItem('searchChatModelProvider');
localStorage.removeItem('searchChatModel');
}}
className="p-1.5 rounded-md hover:bg-light-200 dark:hover:bg-dark-200 text-black/50 dark:text-white/50 hover:text-black/80 dark:hover:text-white/80 transition-colors"
className="p-1.5 rounded-md hover:bg-surface-2 transition-colors"
title="Reset chat model"
>
<RotateCcw size={16} />
@ -1255,9 +1227,7 @@ export default function SettingsPage() {
{config.chatModelProviders && (
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Model Provider
</p>
<p className="text-sm">Chat Model Provider</p>
<Select
value={selectedChatModelProvider ?? undefined}
onChange={(e) => {
@ -1286,9 +1256,7 @@ export default function SettingsPage() {
{selectedChatModelProvider &&
selectedChatModelProvider != 'custom_openai' && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Model
</p>
<p className="text-sm">Chat Model</p>
<Select
value={selectedChatModel ?? undefined}
onChange={(e) => {
@ -1326,9 +1294,7 @@ export default function SettingsPage() {
/>
{selectedChatModelProvider === 'ollama' && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Context Window Size
</p>
<p className="text-sm">Chat Context Window Size</p>
<Select
value={
isCustomContextWindow
@ -1389,7 +1355,7 @@ export default function SettingsPage() {
/>
</div>
)}
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
<p className="text-xs mt-0.5">
{isCustomContextWindow
? 'Adjust the context window size for Ollama models (minimum 512 tokens)'
: 'Adjust the context window size for Ollama models'}
@ -1405,9 +1371,7 @@ export default function SettingsPage() {
selectedChatModelProvider === 'custom_openai' && (
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Model Name
</p>
<p className="text-sm">Model Name</p>
<InputComponent
type="text"
placeholder="Model name"
@ -1425,9 +1389,7 @@ export default function SettingsPage() {
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Custom OpenAI API Key
</p>
<p className="text-sm">Custom OpenAI API Key</p>
<InputComponent
type="password"
placeholder="Custom OpenAI API Key"
@ -1445,9 +1407,7 @@ export default function SettingsPage() {
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Custom OpenAI Base URL
</p>
<p className="text-sm">Custom OpenAI Base URL</p>
<InputComponent
type="text"
placeholder="Custom OpenAI Base URL"
@ -1468,11 +1428,9 @@ export default function SettingsPage() {
)}
{config.embeddingModelProviders && (
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-surface-2">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding Model Provider
</p>
<p className="text-sm">Embedding Model Provider</p>
<Select
value={selectedEmbeddingModelProvider ?? undefined}
onChange={(e) => {
@ -1500,9 +1458,7 @@ export default function SettingsPage() {
{selectedEmbeddingModelProvider && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Embedding Model
</p>
<p className="text-sm">Embedding Model</p>
<Select
value={selectedEmbeddingModel ?? undefined}
onChange={(e) => {
@ -1591,35 +1547,29 @@ export default function SettingsPage() {
return (
<div
key={providerId}
className="border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden"
className="border border-surface-2 rounded-lg overflow-hidden"
>
<button
onClick={() => toggleProviderExpansion(providerId)}
className="w-full p-3 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition-colors flex items-center justify-between"
className="w-full p-3 bg-surface hover:bg-surface-2 transition-colors flex items-center justify-between"
>
<div className="flex items-center space-x-3">
{isExpanded ? (
<ChevronDown
size={16}
className="text-black/70 dark:text-white/70"
/>
<ChevronDown size={16} />
) : (
<ChevronRight
size={16}
className="text-black/70 dark:text-white/70"
/>
<ChevronRight size={16} />
)}
<h4 className="text-sm font-medium text-black/80 dark:text-white/80">
<h4 className="text-sm font-medium">
{(PROVIDER_METADATA as any)[provider]
?.displayName ||
provider.charAt(0).toUpperCase() +
provider.slice(1)}
</h4>
</div>
<div className="flex items-center space-x-2 text-xs text-black/60 dark:text-white/60">
<div className="flex items-center space-x-2 text-xs">
<span>{totalCount - hiddenCount} visible</span>
{hiddenCount > 0 && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded">
<span className="px-2 py-1 bg-red-100 text-red-700 rounded">
{hiddenCount} hidden
</span>
)}
@ -1627,7 +1577,7 @@ export default function SettingsPage() {
</button>
{isExpanded && (
<div className="p-3 bg-light-100 dark:bg-dark-100 border-t border-light-200 dark:border-dark-200">
<div className="p-3 bg-surface-2 border-t border-surface-2">
<div className="flex justify-end mb-3 space-x-2">
<button
onClick={(e) => {
@ -1637,7 +1587,7 @@ export default function SettingsPage() {
true,
);
}}
className="px-3 py-1.5 text-xs rounded-md bg-green-100 hover:bg-green-200 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-700 dark:text-green-400 flex items-center gap-1.5 transition-colors"
className="px-3 py-1.5 text-xs rounded-md bg-green-100 hover:bg-green-200 text-green-700 flex items-center gap-1.5 transition-colors"
title="Show all models in this provider"
>
<Eye size={14} />
@ -1651,7 +1601,7 @@ export default function SettingsPage() {
false,
);
}}
className="px-3 py-1.5 text-xs rounded-md bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-700 dark:text-red-400 flex items-center gap-1.5 transition-colors"
className="px-3 py-1.5 text-xs rounded-md bg-red-100 hover:bg-red-200 text-red-700 flex items-center gap-1.5 transition-colors"
title="Hide all models in this provider"
>
<EyeOff size={14} />
@ -1662,9 +1612,9 @@ export default function SettingsPage() {
{modelEntries.map(([modelKey, model]) => (
<div
key={`${provider}-${modelKey}`}
className="flex items-center justify-between p-2 bg-white dark:bg-dark-secondary rounded-md"
className="flex items-center justify-between p-2 bg-surface rounded-md"
>
<span className="text-sm text-black/90 dark:text-white/90">
<span className="text-sm">
{model.displayName || modelKey}
</span>
<Switch
@ -1677,8 +1627,8 @@ export default function SettingsPage() {
}}
className={cn(
!hiddenModels.includes(modelKey)
? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200',
? 'bg-accent'
: 'bg-surface-2',
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none',
)}
>
@ -1700,9 +1650,7 @@ export default function SettingsPage() {
);
})
) : (
<p className="text-sm text-black/60 dark:text-white/60 italic">
No models available
</p>
<p className="text-sm italic">No models available</p>
);
})()}
</div>
@ -1714,9 +1662,7 @@ export default function SettingsPage() {
>
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
OpenAI API Key
</p>
<p className="text-sm">OpenAI API Key</p>
<InputComponent
type="password"
placeholder="OpenAI API Key"
@ -1733,9 +1679,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Ollama API URL
</p>
<p className="text-sm">Ollama API URL</p>
<InputComponent
type="text"
placeholder="Ollama API URL"
@ -1752,9 +1696,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
GROQ API Key
</p>
<p className="text-sm">GROQ API Key</p>
<InputComponent
type="password"
placeholder="GROQ API Key"
@ -1771,9 +1713,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
OpenRouter API Key
</p>
<p className="text-sm">OpenRouter API Key</p>
<InputComponent
type="password"
placeholder="OpenRouter API Key"
@ -1790,9 +1730,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Anthropic API Key
</p>
<p className="text-sm">Anthropic API Key</p>
<InputComponent
type="password"
placeholder="Anthropic API key"
@ -1809,9 +1747,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Gemini API Key
</p>
<p className="text-sm">Gemini API Key</p>
<InputComponent
type="password"
placeholder="Gemini API key"
@ -1828,9 +1764,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Deepseek API Key
</p>
<p className="text-sm">Deepseek API Key</p>
<InputComponent
type="password"
placeholder="Deepseek API Key"
@ -1847,9 +1781,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
AI/ML API Key
</p>
<p className="text-sm">AI/ML API Key</p>
<InputComponent
type="text"
placeholder="AI/ML API Key"
@ -1866,9 +1798,7 @@ export default function SettingsPage() {
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
LM Studio API URL
</p>
<p className="text-sm">LM Studio API URL</p>
<InputComponent
type="text"
placeholder="LM Studio API URL"

View file

@ -232,7 +232,7 @@ const Chat = ({
onThinkBoxToggle={onThinkBoxToggle}
/>
{!isLast && msg.role === 'assistant' && (
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="h-px w-full bg-surface-2" />
)}
</Fragment>
);
@ -248,7 +248,7 @@ const Chat = ({
setIsAtBottom(true);
messageEnd.current?.scrollIntoView({ behavior: 'smooth' });
}}
className="bg-[#24A0ED] text-white hover:bg-opacity-85 transition duration-100 rounded-full px-4 py-2 shadow-lg flex items-center justify-center"
className="bg-accent text-fg hover:bg-opacity-85 transition duration-100 rounded-full px-4 py-2 shadow-lg flex items-center justify-center"
aria-label="Scroll to bottom"
>
<svg

View file

@ -812,7 +812,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
</Link>
</div>
<div className="flex flex-col items-center justify-center min-h-screen">
<p className="dark:text-white/70 text-black/70 text-sm">
<p className="text-sm">
Failed to connect to the server. Please try again later.
</p>
</div>
@ -870,7 +870,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
<div className="flex flex-row items-center justify-center min-h-screen">
<svg
aria-hidden="true"
className="w-8 h-8 text-light-200 fill-light-secondary dark:text-[#202020] animate-spin dark:fill-[#ffffff3b]"
className="w-8 h-8 text-fg/20 fill-fg/30 animate-spin"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View file

@ -19,7 +19,7 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
href={url}
target="_blank"
rel="noopener noreferrer"
className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative hover:bg-light-200 dark:hover:bg-dark-200 transition-colors duration-200"
className="bg-surface px-1 rounded ml-1 no-underline text-xs text-fg/70 relative hover:bg-surface-2 transition-colors duration-200 border border-surface-2"
>
{number}
</a>
@ -64,14 +64,14 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
transform: 'translate(-50%, -100%)',
}}
>
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 shadow-lg w-96">
<div className="bg-surface border rounded-lg border-surface-2 shadow-lg w-96">
<MessageSource
source={source}
className="shadow-none border-none bg-transparent hover:bg-transparent dark:hover:bg-transparent cursor-pointer"
className="shadow-none border-none bg-transparent hover:bg-transparent cursor-pointer"
/>
</div>
{/* Tooltip arrow */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-light-200 dark:border-t-dark-200"></div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-surface-2"></div>
</div>,
document.body,
)}

View file

@ -75,7 +75,7 @@ const DeleteChat = ({
}
}}
>
<DialogBackdrop className="fixed inset-0 bg-black/30" />
<DialogBackdrop className="fixed inset-0 bg-fg/30" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild
@ -87,11 +87,11 @@ const DeleteChat = ({
leaveFrom="opacity-100 scale-200"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-surface border border-surface-2 p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle className="text-lg font-medium leading-6">
Delete Confirmation
</DialogTitle>
<Description className="text-sm dark:text-white/70 text-black/70">
<Description className="text-sm">
Are you sure you want to delete this chat?
</Description>
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
@ -101,7 +101,7 @@ const DeleteChat = ({
setConfirmationDialogOpen(false);
}
}}
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
className="text-sm transition duration-200"
>
Cancel
</button>

View file

@ -40,9 +40,7 @@ const EmptyChat = ({
</div>
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
<div className="flex flex-col items-center justify-center w-full space-y-8">
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
Research begins here.
</h2>
<h2 className="text-3xl font-medium -mt-8">Research begins here.</h2>
<MessageInput
firstMessage={true}
loading={false}

View file

@ -7,7 +7,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
const isDashboard = segments.includes('dashboard');
return (
<main className="lg:pl-20 bg-light-primary dark:bg-dark-primary min-h-screen">
<main className="lg:pl-20 bg-bg min-h-screen">
<div className={isDashboard ? 'mx-4' : 'max-w-screen-lg lg:mx-auto mx-4'}>
{children}
</div>

View file

@ -11,13 +11,12 @@ import {
Settings,
} from 'lucide-react';
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
oneDark,
oneLight,
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { useTheme } from 'next-themes';
import ThinkBox from './ThinkBox';
import { Document } from '@langchain/core/documents';
import CitationLink from './CitationLink';
@ -81,23 +80,15 @@ const ToolCall = ({
switch (toolType) {
case 'search':
case 'web_search':
return (
<Search size={16} className="text-blue-600 dark:text-blue-400" />
);
return <Search size={16} className="text-accent" />;
case 'file':
case 'file_search':
return (
<FileText size={16} className="text-green-600 dark:text-green-400" />
);
return <FileText size={16} className="text-green-600" />;
case 'url':
case 'url_summarization':
return (
<Globe size={16} className="text-purple-600 dark:text-purple-400" />
);
return <Globe size={16} className="text-purple-600" />;
default:
return (
<Settings size={16} className="text-gray-600 dark:text-gray-400" />
);
return <Settings size={16} className="text-fg/70" />;
}
};
@ -106,8 +97,8 @@ const ToolCall = ({
return (
<>
<span className="mr-2">{getIcon(type)}</span>
<span className="text-black/60 dark:text-white/60">Web search:</span>
<span className="ml-2 px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded font-mono text-sm">
<span>Web search:</span>
<span className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm">
{query || children}
</span>
</>
@ -118,8 +109,8 @@ const ToolCall = ({
return (
<>
<span className="mr-2">{getIcon(type)}</span>
<span className="text-black/60 dark:text-white/60">File search:</span>
<span className="ml-2 px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded font-mono text-sm">
<span>File search:</span>
<span className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm">
{query || children}
</span>
</>
@ -131,7 +122,7 @@ const ToolCall = ({
return (
<>
<span className="mr-2">{getIcon(type)}</span>
<span className="text-black/60 dark:text-white/60">
<span>
Analyzing {urlCount} web page{urlCount === 1 ? '' : 's'} for
additional details
</span>
@ -143,8 +134,8 @@ const ToolCall = ({
return (
<>
<span className="mr-2">{getIcon(type || 'default')}</span>
<span className="text-black/60 dark:text-white/60">Using tool:</span>
<span className="ml-2 px-2 py-0.5 bg-black/5 dark:bg-white/5 rounded font-mono text-sm border">
<span>Using tool:</span>
<span className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm border border-surface-2">
{type || 'unknown'}
</span>
</>
@ -152,7 +143,7 @@ const ToolCall = ({
};
return (
<div className="my-3 px-4 py-3 bg-gradient-to-r from-blue-50/50 to-purple-50/50 dark:from-blue-900/20 dark:to-purple-900/20 border border-blue-200/30 dark:border-blue-700/30 rounded-lg">
<div className="my-3 px-4 py-3 bg-surface border border-surface-2 rounded-lg">
<div className="flex items-center text-sm font-medium">
{formatToolMessage()}
</div>
@ -190,7 +181,24 @@ const CodeBlock = ({
className?: string;
children: React.ReactNode;
}) => {
const { theme } = useTheme();
// Determine dark mode based on html.dark class so custom themes are respected
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const getIsDark = () =>
typeof document !== 'undefined' &&
document.documentElement.classList.contains('dark');
setIsDark(getIsDark());
const observer = new MutationObserver(() => setIsDark(getIsDark()));
if (typeof document !== 'undefined') {
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
}
return () => observer.disconnect();
}, []);
// Extract language from className (format could be "language-javascript" or "lang-javascript")
let language = '';
@ -211,23 +219,23 @@ const CodeBlock = ({
setTimeout(() => setIsCopied(false), 2000);
};
// Choose syntax highlighting style based on theme
const syntaxStyle = theme === 'light' ? oneLight : oneDark;
const backgroundStyle = theme === 'light' ? '#fafafa' : '#1c1c1c';
// Choose syntax highlighting style based on actual dark/light class
const syntaxStyle = isDark ? oneDark : oneLight;
const backgroundStyle = isDark ? '#1c1c1c' : '#fafafa';
return (
<div className="rounded-md overflow-hidden my-4 relative group border border-light-200 dark:border-dark-secondary">
<div className="flex justify-between items-center px-4 py-2 bg-light-100 dark:bg-dark-200 border-b border-light-200 dark:border-dark-secondary text-xs text-black/70 dark:text-white/70 font-mono">
<div className="rounded-md overflow-hidden my-4 relative group border border-surface-2">
<div className="flex justify-between items-center px-4 py-2 bg-surface-2 border-b border-surface-2 text-xs text-fg/70 font-mono">
<span>{language}</span>
<button
onClick={handleCopyCode}
className="p-1 rounded-md hover:bg-light-200 dark:hover:bg-dark-secondary transition duration-200"
className="p-1 rounded-md hover:bg-surface transition duration-200"
aria-label="Copy code to clipboard"
>
{isCopied ? (
<CheckCheck size={14} className="text-green-500" />
) : (
<CopyIcon size={14} className="text-black/70 dark:text-white/70" />
<CopyIcon size={14} className="text-fg" />
)}
</button>
</div>
@ -312,7 +320,7 @@ const MarkdownRenderer = ({
}
// This is an inline code block (`code`)
return (
<code className="px-1.5 py-0.5 rounded bg-light-200 dark:bg-dark-secondary text-black dark:text-white font-mono text-sm">
<code className="px-1.5 py-0.5 rounded bg-surface-2 font-mono text-sm">
{children}
</code>
);
@ -320,9 +328,7 @@ const MarkdownRenderer = ({
},
strong: {
component: ({ children }) => (
<strong className="font-bold text-black dark:text-white">
{children}
</strong>
<strong className="font-bold">{children}</strong>
),
},
pre: {
@ -364,11 +370,11 @@ const MarkdownRenderer = ({
<div className="relative">
<Markdown
className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'prose-code:bg-transparent prose-code:p-0 prose-code:text-inherit prose-code:font-normal prose-code:before:content-none prose-code:after:content-none',
'prose-pre:bg-transparent prose-pre:border-0 prose-pre:m-0 prose-pre:p-0',
'prose-strong:text-black dark:prose-strong:text-white prose-strong:font-bold',
'break-words text-black dark:text-white max-w-full',
'prose-strong:font-bold',
'break-words max-w-full',
className,
)}
options={markdownOverrides}

View file

@ -19,7 +19,7 @@ const Copy = ({
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}}
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
className="p-2 rounded-xl transition duration-200"
>
{copied ? <Check size={18} /> : <ClipboardList size={18} />}
</button>

View file

@ -39,7 +39,7 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
<div className="relative">
<button
ref={buttonRef}
className="p-1 ml-1 text-black/70 dark:text-white/70 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
className="p-1 ml-1 rounded-full hover:bg-surface-2 transition duration-200"
onClick={() => setShowPopover(!showPopover)}
aria-label="Show model information"
>
@ -48,25 +48,19 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
{showPopover && (
<div
ref={popoverRef}
className="absolute z-10 left-6 top-0 w-72 rounded-md shadow-lg bg-white dark:bg-dark-secondary border border-light-200 dark:border-dark-200"
className="absolute z-10 left-6 top-0 w-72 rounded-md shadow-lg border border-surface-2 bg-surface"
>
<div className="py-2 px-3">
<h4 className="text-sm font-medium mb-2 text-black dark:text-white">
Model Information
</h4>
<h4 className="text-sm font-medium mb-2">Model Information</h4>
<div className="space-y-1 text-xs">
<div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">Model:</span>
<span className="text-black dark:text-white font-medium">
{modelName}
</span>
<span className="">Model:</span>
<span className="font-medium">{modelName}</span>
</div>
{modelStats?.responseTime && (
<div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">
Response time:
</span>
<span className="text-black dark:text-white font-medium">
<span>Response time:</span>
<span className="font-medium">
{(modelStats.responseTime / 1000).toFixed(2)}s
</span>
</div>
@ -74,26 +68,20 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
{modelStats?.usage && (
<>
<div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">
Input tokens:
</span>
<span className="text-black dark:text-white font-medium">
<span>Input tokens:</span>
<span className="font-medium">
{modelStats.usage.input_tokens.toLocaleString()}
</span>
</div>
<div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">
Output tokens:
</span>
<span className="text-black dark:text-white font-medium">
<span>Output tokens:</span>
<span className="font-medium">
{modelStats.usage.output_tokens.toLocaleString()}
</span>
</div>
<div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">
Total tokens:
</span>
<span className="text-black dark:text-white font-medium">
<span>Total tokens:</span>
<span className="font-medium">
{modelStats.usage.total_tokens.toLocaleString()}
</span>
</div>

View file

@ -10,7 +10,7 @@ const Rewrite = ({
return (
<button
onClick={() => rewrite(messageId)}
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
className="py-2 px-3 rounded-xl hover:bg-secondary transition duration-200 flex flex-row items-center space-x-1"
>
<ArrowLeftRight size={18} />
<p className="text-xs font-medium">Rewrite</p>

View file

@ -70,7 +70,7 @@ const MessageBox = ({
{isEditing ? (
<div className="w-full">
<textarea
className="w-full p-3 text-lg bg-light-100 dark:bg-dark-100 rounded-lg border border-light-secondary dark:border-dark-secondary text-black dark:text-white focus:outline-none focus:border-[#24A0ED] transition duration-200 min-h-[120px] font-medium"
className="w-full p-3 text-lg bg-surface rounded-lg transition duration-200 min-h-[120px] font-medium text-fg placeholder:text-fg/40 border border-surface-2 focus:outline-none focus:ring-2 focus:ring-accent/40"
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
placeholder="Edit your message..."
@ -79,7 +79,7 @@ const MessageBox = ({
<div className="flex flex-row space-x-2 mt-3 justify-end">
<button
onClick={cancelEditMessage}
className="p-2 rounded-full bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
className="p-2 rounded-full bg-surface hover:bg-surface-2 border border-surface-2 transition duration-200 text-fg/80"
aria-label="Cancel"
title="Cancel"
>
@ -87,27 +87,24 @@ const MessageBox = ({
</button>
<button
onClick={saveEditMessage}
className="p-2 rounded-full bg-[#24A0ED] hover:bg-[#1a8ad3] transition duration-200 text-white disabled:opacity-50 disabled:cursor-not-allowed"
className="p-2 rounded-full bg-accent hover:bg-accent-700 transition duration-200 text-white disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Save changes"
title="Save changes"
disabled={!editedContent.trim()}
>
<Check size={18} className="text-white" />
<Check size={18} />
</button>
</div>
</div>
) : (
<>
<div className="flex items-center">
<h2
className="text-black dark:text-white font-medium text-3xl"
onClick={startEditMessage}
>
<h2 className="font-medium text-3xl" onClick={startEditMessage}>
{message.content}
</h2>
<button
onClick={startEditMessage}
className="ml-3 p-2 rounded-xl bg-light-secondary dark:bg-dark-secondary text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white flex-shrink-0"
className="ml-3 p-2 rounded-xl bg-surface hover:bg-surface-2 border border-surface-2 flex-shrink-0"
aria-label="Edit message"
title="Edit message"
>

View file

@ -11,22 +11,17 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
return (
<div className="flex flex-col space-y-4 w-full lg:w-9/12">
{progress && progress.current !== progress.total ? (
<div className="bg-light-primary dark:bg-dark-primary rounded-lg p-4">
<div className="bg-surface rounded-lg p-4 border border-surface-2">
<div className="flex flex-col space-y-3">
<p className="text-base font-semibold text-black dark:text-white">
{progress.message}
</p>
<p className="text-base font-semibold">{progress.message}</p>
{progress.subMessage && (
<p
className="text-xs text-black/40 dark:text-white/40 mt-1"
title={progress.subMessage}
>
<p className="text-xs mt-1" title={progress.subMessage}>
{progress.subMessage}
</p>
)}
<div className="w-full bg-light-secondary dark:bg-dark-secondary rounded-full h-2 overflow-hidden">
<div className="w-full bg-surface-2 rounded-full h-2 overflow-hidden">
<div
className={`h-full bg-[#24A0ED] transition-all duration-300 ease-in-out ${
className={`h-full bg-accent transition-all duration-300 ease-in-out ${
progress.current === progress.total ? '' : 'animate-pulse'
}`}
style={{
@ -39,9 +34,9 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
) : (
<div className="pl-3 flex items-center justify-start">
<div className="flex space-x-1">
<div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.3s]"></div>
<div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-[high-bounce_1s_infinite]"></div>
<div className="w-1.5 h-1.5 bg-fg/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.3s]"></div>
<div className="w-1.5 h-1.5 bg-fg/40 rounded-full animate-[high-bounce_1s_infinite] [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-fg/40 rounded-full animate-[high-bounce_1s_infinite]"></div>
</div>
</div>
)}

View file

@ -141,14 +141,14 @@ const MessageInput = ({
}}
className="w-full"
>
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-3 pt-4 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
<div className="flex flex-row items-end space-x-2 mb-2">
<div className="flex flex-col bg-surface px-3 pt-4 pb-2 rounded-lg w-full border border-surface-2">
<div className="flex flex-row space-x-2 mb-2">
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
minRows={1}
className="mb-2 bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
className="px-3 py-2 overflow-hidden flex rounded-lg bg-transparent text-sm resize-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder={firstMessage ? 'Ask anything...' : 'Ask a follow-up'}
autoFocus={true}
/>
@ -196,7 +196,7 @@ const MessageInput = ({
aria-label="Cancel"
>
{loading && (
<div className="absolute inset-0 rounded-full border-2 border-white/30 border-t-white animate-spin" />
<div className="absolute inset-0 rounded-full border-2 border-fg/30 border-t-fg animate-spin" />
)}
<span className="relative flex items-center justify-center w-[17px] h-[17px]">
<Square size={17} className="text-white" />
@ -205,13 +205,13 @@ const MessageInput = ({
) : (
<button
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
className="bg-accent text-white disabled:text-white/50 disabled:bg-accent/20 hover:bg-accent-700 transition duration-100 rounded-full p-2"
type="submit"
>
{firstMessage ? (
<ArrowRight className="bg-background" size={17} />
<ArrowRight size={17} />
) : (
<ArrowUp className="bg-background" size={17} />
<ArrowUp size={17} />
)}
</button>
)}

View file

@ -84,20 +84,20 @@ const Attach = ({
'flex flex-row items-center justify-between space-x-1 p-2 rounded-xl transition duration-200',
files.length > 0 ? '-ml-2 lg:-ml-3' : '',
isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
: 'text-black/50 dark:text-white/50 hover:bg-light-secondary dark:hover:bg-dark-secondary hover:text-black dark:hover:text-white',
? 'text-fg/20 cursor-not-allowed'
: 'text-fg/50 hover:bg-surface-2 hover:text-fg',
)}
>
{files.length > 1 && (
<>
<File
size={19}
className={isDisabled ? 'text-sky-900' : 'text-sky-400'}
className={isDisabled ? 'text-fg/20' : 'text-accent'}
/>
<p
className={cn(
'inline whitespace-nowrap text-xs font-medium',
isDisabled ? 'text-sky-900' : 'text-sky-400',
isDisabled ? 'text-fg/20' : 'text-accent',
)}
>
{files.length} files
@ -109,12 +109,12 @@ const Attach = ({
<>
<File
size={18}
className={isDisabled ? 'text-sky-900' : 'text-sky-400'}
className={isDisabled ? 'text-fg/20' : 'text-accent'}
/>
<p
className={cn(
'text-xs font-medium',
isDisabled ? 'text-sky-900' : 'text-sky-400',
isDisabled ? 'text-fg/20' : 'text-accent',
)}
>
{files[0].fileName.length > 10
@ -136,11 +136,9 @@ const Attach = ({
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-64 md:w-[350px] right-0">
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
<div className="bg-surface border rounded-md border-surface-2 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
<div className="flex flex-row items-center justify-between px-3 py-2">
<h4 className="text-black dark:text-white font-medium text-sm">
Attached files
</h4>
<h4 className="text-fg font-medium text-sm">Attached files</h4>
<div className="flex flex-row items-center space-x-4">
<button
type="button"
@ -149,8 +147,8 @@ const Attach = ({
className={cn(
'flex flex-row items-center space-x-1 transition duration-200',
isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white',
? 'text-fg/20 cursor-not-allowed'
: 'text-fg/70 hover:text-fg',
)}
>
<input
@ -176,8 +174,8 @@ const Attach = ({
className={cn(
'flex flex-row items-center space-x-1 transition duration-200',
isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white',
? 'text-fg/20 cursor-not-allowed'
: 'text-fg/70 hover:text-fg',
)}
>
<Trash size={14} />
@ -185,17 +183,17 @@ const Attach = ({
</button>
</div>
</div>
<div className="h-[0.5px] mx-2 bg-white/10" />
<div className="h-[0.5px] mx-2 bg-surface-2" />
<div className="flex flex-col items-center">
{files.map((file, i) => (
<div
key={i}
className="flex flex-row items-center justify-start w-full space-x-3 p-3"
>
<div className="bg-dark-100 flex items-center justify-center w-10 h-10 rounded-md">
<File size={16} className="text-white/70" />
<div className="bg-surface-2 flex items-center justify-center w-10 h-10 rounded-md">
<File size={16} className="text-fg/70" />
</div>
<p className="text-black/70 dark:text-white/70 text-sm">
<p className="text-fg/70 text-sm">
{file.fileName.length > 25
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
'...' +
@ -211,9 +209,9 @@ const Attach = ({
</Popover>
{isSpeedMode && (
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
<div className="bg-black dark:bg-white text-white dark:text-black text-xs px-2 py-1 rounded whitespace-nowrap">
<div className="bg-fg text-bg text-xs px-2 py-1 rounded whitespace-nowrap">
File attachments are disabled in Speed mode
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black dark:border-t-white"></div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-fg"></div>
</div>
</div>
)}
@ -227,8 +225,8 @@ const Attach = ({
className={cn(
'flex flex-row items-center space-x-1 rounded-xl transition duration-200 p-2',
isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed'
: 'text-black/50 dark:text-white/50 hover:bg-light-secondary dark:hover:bg-dark-secondary hover:text-black dark:hover:text-white',
? 'text-fg/20 cursor-not-allowed'
: 'text-fg/50 hover:bg-surface-2 hover:text-fg',
)}
>
<input
@ -244,9 +242,9 @@ const Attach = ({
</button>
{isSpeedMode && (
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
<div className="bg-black dark:bg-white text-white dark:text-black text-xs px-2 py-1 rounded whitespace-nowrap">
<div className="bg-fg text-bg text-xs px-2 py-1 rounded whitespace-nowrap">
File attachments are disabled in Speed mode
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black dark:border-t-white"></div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-fg"></div>
</div>
</div>
)}

View file

@ -1,43 +0,0 @@
import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react';
const CopilotToggle = ({
copilotEnabled,
setCopilotEnabled,
}: {
copilotEnabled: boolean;
setCopilotEnabled: (enabled: boolean) => void;
}) => {
return (
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
<Switch
checked={copilotEnabled}
onChange={setCopilotEnabled}
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
>
<span className="sr-only">Copilot</span>
<span
className={cn(
copilotEnabled
? 'translate-x-6 bg-[#24A0ED]'
: 'translate-x-1 bg-black/50 dark:bg-white/50',
'inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200',
)}
/>
</Switch>
<p
onClick={() => setCopilotEnabled(!copilotEnabled)}
className={cn(
'text-xs font-medium transition-colors duration-150 ease-in-out',
copilotEnabled
? 'text-[#24A0ED]'
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
)}
>
Copilot
</p>
</div>
);
};
export default CopilotToggle;

View file

@ -7,7 +7,7 @@ const focusModes = [
key: 'webSearch',
title: 'All',
description: 'Searches across all of the internet',
icon: <Globe size={20} className="text-[#24A0ED]" />,
icon: <Globe size={20} className="text-accent" />,
},
{
key: 'chat',
@ -42,17 +42,17 @@ const Focus = ({
);
return (
<div className="text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white">
<div className="rounded-xl hover:bg-surface-2 transition duration-200">
<div className="flex flex-row items-center space-x-1">
<div className="relative">
<div className="flex items-center border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden">
<div className="flex items-center border border-surface-2 rounded-lg overflow-hidden">
{/* Web Search Mode Icon */}
<button
className={cn(
'p-2 transition-all duration-200',
focusMode === 'webSearch'
? 'bg-[#24A0ED]/20 text-[#24A0ED] scale-105'
: 'text-black/30 dark:text-white/30 hover:text-black/50 dark:hover:text-white/50 hover:bg-light-secondary/50 dark:hover:bg-dark-secondary/50',
? 'text-accent scale-105'
: 'text-fg/70',
)}
onMouseEnter={() => setShowWebSearchTooltip(true)}
onMouseLeave={() => setShowWebSearchTooltip(false)}
@ -65,15 +65,15 @@ const Focus = ({
</button>
{/* Divider */}
<div className="h-6 w-px bg-light-200 dark:bg-dark-200"></div>
<div className="h-6 w-px border-l opacity-10"></div>
{/* Chat Mode Icon */}
<button
className={cn(
'p-2 transition-all duration-200',
focusMode === 'chat'
? 'bg-[#10B981]/20 text-[#10B981] scale-105'
: 'text-black/30 dark:text-white/30 hover:text-black/50 dark:hover:text-white/50 hover:bg-light-secondary/50 dark:hover:bg-dark-secondary/50',
? 'text-[#10B981] scale-105'
: 'text-fg/70',
)}
onMouseEnter={() => setShowChatTooltip(true)}
onMouseLeave={() => setShowChatTooltip(false)}
@ -86,15 +86,15 @@ const Focus = ({
</button>
{/* Divider */}
<div className="h-6 w-px bg-light-200 dark:bg-dark-200"></div>
<div className="h-6 w-px border-l opacity-10"></div>
{/* Local Research Mode Icon */}
<button
className={cn(
'p-2 transition-all duration-200',
focusMode === 'localResearch'
? 'bg-[#8B5CF6]/20 text-[#8B5CF6] scale-105'
: 'text-black/30 dark:text-white/30 hover:text-black/50 dark:hover:text-white/50 hover:bg-light-secondary/50 dark:hover:bg-dark-secondary/50',
? 'text-[#8B5CF6] scale-105'
: 'text-fg/70',
)}
onMouseEnter={() => setShowLocalResearchTooltip(true)}
onMouseLeave={() => setShowLocalResearchTooltip(false)}
@ -110,14 +110,14 @@ const Focus = ({
{/* Web Search Mode Tooltip */}
{showWebSearchTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 animate-in fade-in-0 duration-150">
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 p-4 w-80 shadow-lg">
<div className="bg-surface border rounded-lg border-surface-2 p-4 w-80 shadow-lg">
<div className="flex items-center space-x-2 mb-2">
<Globe size={16} className="text-[#24A0ED]" />
<h3 className="font-medium text-sm text-black dark:text-white text-left">
<Globe size={16} className="text-accent" />
<h3 className="font-medium text-sm text-left">
{webSearchMode?.title}
</h3>
</div>
<p className="text-sm text-black/70 dark:text-white/70 leading-relaxed text-left">
<p className="text-sm leading-relaxed text-left">
{webSearchMode?.description}
</p>
</div>
@ -127,14 +127,14 @@ const Focus = ({
{/* Chat Mode Tooltip */}
{showChatTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 transform animate-in fade-in-0 duration-150">
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 p-4 w-80 shadow-lg">
<div className="bg-surface border rounded-lg border-surface-2 p-4 w-80 shadow-lg">
<div className="flex items-center space-x-2 mb-2">
<MessageCircle size={16} className="text-[#10B981]" />
<h3 className="font-medium text-sm text-black dark:text-white text-left">
<h3 className="font-medium text-sm text-left">
{chatMode?.title}
</h3>
</div>
<p className="text-sm text-black/70 dark:text-white/70 leading-relaxed text-left">
<p className="text-sm leading-relaxed text-left">
{chatMode?.description}
</p>
</div>
@ -144,14 +144,14 @@ const Focus = ({
{/* Local Research Mode Tooltip */}
{showLocalResearchTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 animate-in fade-in-0 duration-150">
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 p-4 w-80 shadow-lg">
<div className="bg-surface border rounded-lg border-surface-2 p-4 w-80 shadow-lg">
<div className="flex items-center space-x-2 mb-2">
<Pencil size={16} className="text-[#8B5CF6]" />
<h3 className="font-medium text-sm text-black dark:text-white text-left">
<h3 className="font-medium text-smtext-left">
{localResearchMode?.title}
</h3>
</div>
<p className="text-sm text-black/70 dark:text-white/70 leading-relaxed text-left">
<p className="text-sm leading-relaxed text-left">
{localResearchMode?.description}
</p>
</div>

View file

@ -170,7 +170,7 @@ const ModelSelector = ({
<div className="relative">
<PopoverButton
type="button"
className="p-2 group flex text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
className="p-2 group flex text-fg/50 rounded-xl hover:bg-surface-2 active:scale-95 transition duration-200 hover:text-fg"
>
<Cpu size={18} />
{showModelName && (
@ -205,22 +205,22 @@ const ModelSelector = ({
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary divide-y divide-light-200 dark:divide-dark-200">
<div className="overflow-hidden rounded-lg shadow-lg bg-surface border border-surface-2 divide-y divide-surface-2">
<div className="px-4 py-3">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
<h3 className="text-sm font-medium text-fg/90">
Select Model
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-1">
<p className="text-xs text-fg/60 mt-1">
Choose a provider and model for your conversation
</p>
</div>
<div className="max-h-72 overflow-y-auto">
{loading ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
<div className="px-4 py-3 text-sm text-fg/70">
Loading available models...
</div>
) : providersList.length === 0 ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
<div className="px-4 py-3 text-sm text-fg/70">
No models available
</div>
) : (
@ -232,15 +232,15 @@ const ModelSelector = ({
return (
<div
key={providerKey}
className="border-t border-light-200 dark:border-dark-200 first:border-t-0"
className="border-t border-surface-2 first:border-t-0"
>
{/* Provider header */}
<button
className={cn(
'w-full flex items-center justify-between px-4 py-2 text-sm text-left',
'hover:bg-light-100 dark:hover:bg-dark-100',
'hover:bg-surface-2',
selectedModel?.provider === providerKey
? 'bg-light-50 dark:bg-dark-50'
? 'bg-surface-2'
: '',
)}
onClick={() =>
@ -248,13 +248,10 @@ const ModelSelector = ({
}
>
<div className="font-medium flex items-center">
<Cpu
size={14}
className="mr-2 text-black/70 dark:text-white/70"
/>
<Cpu size={14} className="mr-2 text-fg/70" />
{provider.displayName}
{selectedModel?.provider === providerKey && (
<span className="ml-2 text-xs text-[#24A0ED]">
<span className="ml-2 text-xs text-accent">
(active)
</span>
)}
@ -280,8 +277,8 @@ const ModelSelector = ({
modelOption.provider &&
selectedModel?.model ===
modelOption.model
? 'bg-light-100 dark:bg-dark-100 text-black dark:text-white'
: 'text-black/70 dark:text-white/70 hover:bg-light-100 dark:hover:bg-dark-100',
? 'bg-surface-2 text-fg'
: 'text-fg/70 hover:bg-surface-2',
)}
onClick={() =>
handleSelectModel(modelOption)
@ -297,7 +294,7 @@ const ModelSelector = ({
modelOption.provider &&
selectedModel?.model ===
modelOption.model && (
<div className="ml-auto bg-[#24A0ED] text-white text-xs px-1.5 py-0.5 rounded">
<div className="ml-auto bg-accent text-white text-xs px-1.5 py-0.5 rounded">
Active
</div>
)}

View file

@ -47,18 +47,18 @@ const Optimization = ({
<button
type="button"
onClick={handleToggle}
className="text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
className="text-fg/50 rounded-xl hover:bg-surface-2 active:scale-95 transition duration-200 hover:text-fg"
>
<div className="flex flex-row items-center space-x-1">
<div className="relative">
<div className="flex items-center border border-light-200 dark:border-dark-200 rounded-lg overflow-hidden">
<div className="flex items-center border border-surface-2 rounded-lg overflow-hidden">
{/* Speed Mode Icon */}
<div
className={cn(
'p-2 transition-all duration-200',
!isAgentMode
? 'bg-[#FF9800]/20 text-[#FF9800] scale-105'
: 'text-black/30 dark:text-white/30 hover:text-black/50 dark:hover:text-white/50 hover:bg-light-secondary/50 dark:hover:bg-dark-secondary/50',
: 'text-fg/30 hover:text-fg/50 hover:bg-surface-2/50',
)}
onMouseEnter={() => setShowSpeedTooltip(true)}
onMouseLeave={() => setShowSpeedTooltip(false)}
@ -67,7 +67,7 @@ const Optimization = ({
</div>
{/* Divider */}
<div className="h-6 w-px bg-light-200 dark:bg-dark-200"></div>
<div className="h-6 w-px bg-surface-2"></div>
{/* Agent Mode Icon */}
<div
@ -75,7 +75,7 @@ const Optimization = ({
'p-2 transition-all duration-200',
isAgentMode
? 'bg-[#9C27B0]/20 text-[#9C27B0] scale-105'
: 'text-black/30 dark:text-white/30 hover:text-black/50 dark:hover:text-white/50 hover:bg-light-secondary/50 dark:hover:bg-dark-secondary/50',
: 'text-fg/30 hover:text-fg/50 hover:bg-surface-2/50',
)}
onMouseEnter={() => setShowAgentTooltip(true)}
onMouseLeave={() => setShowAgentTooltip(false)}
@ -87,14 +87,14 @@ const Optimization = ({
{/* Speed Mode Tooltip */}
{showSpeedTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 right-0 animate-in fade-in-0 duration-150">
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 p-4 w-80 shadow-lg">
<div className="bg-surface border rounded-lg border-surface-2 p-4 w-80 shadow-lg">
<div className="flex items-center space-x-2 mb-2">
<Zap size={16} className="text-[#FF9800]" />
<h3 className="font-medium text-sm text-black dark:text-white text-left">
<h3 className="font-medium text-sm text-fg text-left">
{speedMode?.title}
</h3>
</div>
<p className="text-sm text-black/70 dark:text-white/70 leading-relaxed text-left">
<p className="text-sm text-fg/70 leading-relaxed text-left">
{speedMode?.description}
</p>
</div>
@ -104,14 +104,14 @@ const Optimization = ({
{/* Agent Mode Tooltip */}
{showAgentTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 right-0 animate-in fade-in-0 duration-150">
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 p-4 w-80 shadow-lg">
<div className="bg-surface border rounded-lg border-surface-2 p-4 w-80 shadow-lg">
<div className="flex items-center space-x-2 mb-2">
<Bot size={16} className="text-[#9C27B0]" />
<h3 className="font-medium text-sm text-black dark:text-white text-left">
<h3 className="font-medium text-sm text-fg text-left">
{agentMode?.title}
</h3>
</div>
<p className="text-sm text-black/70 dark:text-white/70 leading-relaxed text-left">
<p className="text-sm text-fg/70 leading-relaxed text-left">
{agentMode?.description}
</p>
</div>

View file

@ -88,10 +88,10 @@ const SystemPromptSelector = ({
<>
<PopoverButton
className={cn(
'flex items-center gap-1 rounded-lg text-sm transition-colors duration-150 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'flex items-center gap-1 rounded-lg text-sm transition-colors duration-150 ease-in-out focus:outline-none focus-visible:ring-2',
selectedCount > 0
? 'text-[#24A0ED] hover:text-blue-200'
: 'text-black/60 hover:text-black/30 dark:text-white/60 dark:hover:*:text-white/30',
? 'text-accent hover:text-accent'
: 'text-fg/60 hover:text-fg/30',
)}
title="Select Prompts"
>
@ -109,25 +109,25 @@ const SystemPromptSelector = ({
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-20 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary">
<div className="px-4 py-3 border-b border-light-200 dark:border-dark-200">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-surface-2 bg-surface">
<div className="px-4 py-3 border-b border-surface-2">
<h3 className="text-sm font-medium text-fg/90">
Select Prompts
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
<p className="text-xs text-fg/60 mt-0.5">
Choose instructions to guide the AI.
</p>
</div>
{isLoading ? (
<div className="px-4 py-3">
<Loader2 className="animate-spin text-black/70 dark:text-white/70" />
<Loader2 className="animate-spin text-fg/70" />
</div>
) : (
<div className="max-h-60 overflow-y-auto p-1.5 space-y-3">
{availablePrompts.length === 0 && (
<p className="text-xs text-black/50 dark:text-white/50 px-2.5 py-2 text-center">
<p className="text-xs text-fg/50 px-2.5 py-2 text-center">
No prompts configured. <br /> Go to{' '}
<a className="text-blue-500" href="/settings">
<a className="text-accent" href="/settings">
settings
</a>{' '}
to add some.
@ -137,7 +137,7 @@ const SystemPromptSelector = ({
{availablePrompts.filter((p) => p.type === 'system')
.length > 0 && (
<div>
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-black/70 dark:text-white/70">
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-fg/70">
<Settings size={14} />
<span>System Prompts</span>
</div>
@ -148,21 +148,21 @@ const SystemPromptSelector = ({
<div
key={prompt.id}
onClick={() => handleTogglePrompt(prompt.id)}
className="flex items-center gap-2.5 p-2.5 rounded-md hover:bg-light-100 dark:hover:bg-dark-100 cursor-pointer"
className="flex items-center gap-2.5 p-2.5 rounded-md hover:bg-surface-2 cursor-pointer"
>
{selectedPromptIds.includes(prompt.id) ? (
<CheckSquare
size={18}
className="text-[#24A0ED] flex-shrink-0"
className="text-accent flex-shrink-0"
/>
) : (
<Square
size={18}
className="text-black/40 dark:text-white/40 flex-shrink-0"
className="text-fg/40 flex-shrink-0"
/>
)}
<span
className="text-sm text-black/80 dark:text-white/80 truncate"
className="text-sm text-fg/80 truncate"
title={prompt.name}
>
{prompt.name}
@ -176,7 +176,7 @@ const SystemPromptSelector = ({
{availablePrompts.filter((p) => p.type === 'persona')
.length > 0 && (
<div>
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-black/70 dark:text-white/70">
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-fg/70">
<User size={14} />
<span>Persona Prompts</span>
</div>
@ -187,21 +187,21 @@ const SystemPromptSelector = ({
<div
key={prompt.id}
onClick={() => handleTogglePrompt(prompt.id)}
className="flex items-center gap-2.5 p-2.5 rounded-md hover:bg-light-100 dark:hover:bg-dark-100 cursor-pointer"
className="flex items-center gap-2.5 p-2.5 rounded-md hover:bg-surface-2 cursor-pointer"
>
{selectedPromptIds.includes(prompt.id) ? (
<CheckSquare
size={18}
className="text-[#24A0ED] flex-shrink-0"
className="text-accent flex-shrink-0"
/>
) : (
<Square
size={18}
className="text-black/40 dark:text-white/40 flex-shrink-0"
className="text-fg/40 flex-shrink-0"
/>
)}
<span
className="text-sm text-black/80 dark:text-white/80 truncate"
className="text-sm text-fg/80 truncate"
title={prompt.name}
>
{prompt.name}

View file

@ -80,10 +80,10 @@ const ToolSelector = ({
<>
<PopoverButton
className={cn(
'flex items-center gap-1 rounded-lg text-sm transition-colors duration-150 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'flex items-center gap-1 rounded-lg text-sm transition-colors duration-150 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-accent',
selectedCount > 0
? 'text-[#24A0ED] hover:text-blue-200'
: 'text-black/60 hover:text-black/30 dark:text-white/60 dark:hover:*:text-white/30',
? 'text-accent hover:text-accent'
: 'text-fg/60 hover:text-fg/30',
)}
title="Select Tools"
>
@ -101,23 +101,23 @@ const ToolSelector = ({
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-20 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary">
<div className="px-4 py-3 border-b border-light-200 dark:border-dark-200">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
<div className="overflow-hidden rounded-lg shadow-lg bg-surface border border-surface-2">
<div className="px-4 py-3 border-b border-surface-2">
<h3 className="text-sm font-medium text-fg/90">
Select Tools
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
<p className="text-xs text-fg/60 mt-0.5">
Choose tools to assist the AI.
</p>
</div>
{isLoading ? (
<div className="px-4 py-3">
<Loader2 className="animate-spin text-black/70 dark:text-white/70" />
<Loader2 className="animate-spin text-fg/70" />
</div>
) : (
<div className="max-h-60 overflow-y-auto p-1.5 space-y-0.5">
{availableTools.length === 0 && (
<p className="text-xs text-black/50 dark:text-white/50 px-2.5 py-2 text-center">
<p className="text-xs text-fg/50 px-2.5 py-2 text-center">
No tools available.
</p>
)}
@ -126,27 +126,27 @@ const ToolSelector = ({
<div
key={tool.name}
onClick={() => handleToggleTool(tool.name)}
className="flex items-start gap-2.5 p-2.5 rounded-md hover:bg-light-100 dark:hover:bg-dark-100 cursor-pointer"
className="flex items-start gap-2.5 p-2.5 rounded-md hover:bg-surface-2 cursor-pointer"
>
{selectedToolNames.includes(tool.name) ? (
<CheckSquare
size={18}
className="text-[#24A0ED] flex-shrink-0 mt-0.5"
className="text-accent flex-shrink-0 mt-0.5"
/>
) : (
<Square
size={18}
className="text-black/40 dark:text-white/40 flex-shrink-0 mt-0.5"
className="text-fg/40 flex-shrink-0 mt-0.5"
/>
)}
<div className="flex-1 min-w-0">
<span
className="text-sm font-medium text-black/80 dark:text-white/80 block truncate"
className="text-sm font-medium text-fg/80 block truncate"
title={tool.name}
>
{tool.name.replace(/_/g, ' ')}
</span>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
<p className="text-xs text-fg/60 mt-0.5">
{tool.description}
</p>
</div>

View file

@ -17,7 +17,7 @@ const MessageSource = ({
}: MessageSourceProps) => {
return (
<a
className={`bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 rounded-lg p-4 flex flex-row no-underline space-x-3 font-medium ${className || ''}`}
className={`bg-surface hover:bg-surface-2 transition duration-200 rounded-lg p-4 flex flex-row no-underline space-x-3 font-medium border border-surface-2 ${className || ''}`}
href={source.metadata.url}
target="_blank"
style={style}
@ -25,8 +25,8 @@ const MessageSource = ({
{/* Left side: Favicon/Icon and source number */}
<div className="flex flex-col items-center space-y-2 flex-shrink-0">
{source.metadata.url === 'File' ? (
<div className="bg-dark-200 hover:bg-dark-100 transition duration-200 flex items-center justify-center w-8 h-8 rounded-full">
<File size={16} className="text-white/70" />
<div className="bg-surface-2 hover:bg-surface transition duration-200 flex items-center justify-center w-8 h-8 rounded-full">
<File size={16} className="text-fg/70" />
</div>
) : (
<img
@ -37,38 +37,29 @@ const MessageSource = ({
className="rounded-lg h-7 w-7"
/>
)}
<div className="flex flex-row items-center space-x-1 text-black/50 dark:text-white/50 text-xs">
<div className="flex flex-row items-center space-x-1 text-fg/50 text-xs">
{typeof index === 'number' && (
<span className="font-semibold">{index + 1}</span>
)}
{/* Processing type indicator */}
{source.metadata.processingType === 'preview-only' && (
<span title="Partial content analyzed" className="inline-flex">
<Zap size={12} className="text-black/40 dark:text-white/40" />
<Zap size={12} className="text-fg/40" />
</span>
)}
{source.metadata.processingType === 'full-content' && (
<span title="Full content analyzed" className="inline-flex">
<Microscope
size={12}
className="text-black/40 dark:text-white/40"
/>
<Microscope size={12} className="text-fg/40" />
</span>
)}
{source.metadata.processingType === 'url-direct-content' && (
<span title="Direct URL content" className="inline-flex">
<FileText
size={12}
className="text-black/40 dark:text-white/40"
/>
<FileText size={12} className="text-fg/40" />
</span>
)}
{source.metadata.processingType === 'url-content-extraction' && (
<span title="Summarized URL content" className="inline-flex">
<Sparkles
size={12}
className="text-black/40 dark:text-white/40"
/>
<Sparkles size={12} className="text-fg/40" />
</span>
)}
</div>
@ -77,18 +68,18 @@ const MessageSource = ({
{/* Right side: Content */}
<div className="flex-1 flex flex-col space-y-2">
{/* Title */}
<h3 className="dark:text-white text-sm font-semibold leading-tight">
<h3 className="text-fg text-sm font-semibold leading-tight">
{source.metadata.title}
</h3>
{/* URL */}
<p className="text-xs text-black/50 dark:text-white/50">
<p className="text-xs text-fg/50">
{source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')}
</p>
{/* Preview content */}
<p
className="text-xs text-black/70 dark:text-white/70 leading-relaxed overflow-hidden"
className="text-xs text-fg/70 leading-relaxed overflow-hidden"
style={{
display: '-webkit-box',
WebkitLineClamp: 3,

View file

@ -135,7 +135,7 @@ const MessageTabs = ({
const url = source?.metadata?.url;
if (url) {
return `<a href="${url}" target="_blank" data-citation="${number}" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative hover:bg-light-200 dark:hover:bg-dark-200 transition-colors duration-200">${numStr}</a>`;
return `<a href="${url}" target="_blank" data-citation="${number}" className="bg-surface px-1 rounded ml-1 no-underline text-xs relative hover:bg-surface-2 transition-colors duration-200">${numStr}</a>`;
} else {
return `[${numStr}]`;
}
@ -169,14 +169,14 @@ const MessageTabs = ({
return (
<div className="flex flex-col w-full">
{/* Tabs */}
<div className="flex border-b border-light-200 dark:border-dark-200 overflow-x-auto no-scrollbar sticky top-0 bg-light-primary dark:bg-dark-primary z-10 -mx-4 px-4 mb-2 shadow-sm">
<div className="flex border-b border-accent overflow-x-auto no-scrollbar sticky top-0 z-10 -mx-4 px-4 mb-2">
<button
onClick={() => setActiveTab('text')}
className={cn(
'flex items-center px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'text'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
? 'border-b-2 border-accent text-accent bg-surface-2'
: 'hover:bg-surface-2',
)}
aria-selected={activeTab === 'text'}
role="tab"
@ -191,8 +191,8 @@ const MessageTabs = ({
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'sources'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
? 'border-b-2 border-accent text-accent bg-surface-2'
: 'hover:bg-surface-2',
)}
aria-selected={activeTab === 'sources'}
role="tab"
@ -203,8 +203,8 @@ const MessageTabs = ({
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'sources'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
? 'bg-accent/20 text-accent'
: 'bg-surface-2 text-fg/70',
)}
>
{message.sources.length}
@ -217,8 +217,8 @@ const MessageTabs = ({
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'images'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
? 'border-b-2 border-accent text-accent bg-surface-2'
: 'hover:bg-surface-2',
)}
aria-selected={activeTab === 'images'}
role="tab"
@ -230,8 +230,8 @@ const MessageTabs = ({
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'images'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
? 'bg-accent/20 text-accent'
: 'bg-surface-2 text-fg/70',
)}
>
{imageCount}
@ -244,8 +244,8 @@ const MessageTabs = ({
className={cn(
'flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-all duration-200 relative',
activeTab === 'videos'
? 'border-b-2 border-[#24A0ED] text-[#24A0ED] bg-light-100 dark:bg-dark-100'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-light-100 dark:hover:bg-dark-100',
? 'border-b-2 border-accent text-accent bg-surface-2'
: 'hover:bg-surface-2',
)}
aria-selected={activeTab === 'videos'}
role="tab"
@ -257,8 +257,8 @@ const MessageTabs = ({
className={cn(
'ml-1.5 px-1.5 py-0.5 text-xs rounded-full',
activeTab === 'videos'
? 'bg-[#24A0ED]/20 text-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200 text-black/70 dark:text-white/70',
? 'bg-accent/20 text-accent'
: 'bg-surface-2 text-fg/70',
)}
>
{videoCount}
@ -285,7 +285,7 @@ const MessageTabs = ({
sources={message.sources}
/>{' '}
{loading && isLast ? null : (
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white px-4 py-4">
<div className="flex flex-row items-center justify-between w-full px-4 py-4">
<div className="flex flex-row items-center space-x-1">
<Rewrite rewrite={rewrite} messageId={message.messageId} />
{message.modelStats && (
@ -302,7 +302,7 @@ const MessageTabs = ({
start();
}
}}
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
className="p-2 opacity-70 rounded-xl hover:bg-surface-2 transition duration-200"
>
{speechStatus === 'started' ? (
<StopCircle size={18} />
@ -315,7 +315,7 @@ const MessageTabs = ({
)}
{isLast && message.role === 'assistant' && !loading && (
<>
<div className="border-t border-light-secondary dark:border-dark-secondary px-4 pt-4 mt-4">
<div className="border-t border-surface-2 px-4 pt-4 mt-4">
<div className="flex flex-row items-center space-x-2 mb-3">
<Layers3 size={20} />
<h3 className="text-xl font-medium">Related</h3>
@ -325,10 +325,10 @@ const MessageTabs = ({
<button
onClick={handleLoadSuggestions}
disabled={loadingSuggestions}
className="px-4 py-2 flex flex-row items-center justify-center space-x-2 rounded-lg bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
className="px-4 py-2 flex flex-row items-center justify-center space-x-2 rounded-lg bg-surface hover:bg-surface-2 transition duration-200"
>
{loadingSuggestions ? (
<div className="w-4 h-4 border-2 border-t-transparent border-gray-400 dark:border-gray-500 rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-t-transparent border-fg/40 rounded-full animate-spin" />
) : (
<Sparkles size={16} />
)}
@ -348,19 +348,19 @@ const MessageTabs = ({
className="flex flex-col space-y-3 text-sm"
key={i}
>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="h-px w-full bg-surface-2" />
<div
onClick={() => {
sendMessage(suggestion);
}}
className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center"
>
<p className="transition duration-200 hover:text-[#24A0ED]">
<p className="transition duration-200 hover:text-accent">
{suggestion}
</p>
<Plus
size={20}
className="text-[#24A0ED] flex-shrink-0"
className="text-accent flex-shrink-0"
/>
</div>
</div>
@ -379,23 +379,19 @@ const MessageTabs = ({
message.sources.length > 0 && (
<div className="p-4 animate-fadeIn">
{message.searchQuery && (
<div className="mb-4 text-sm bg-light-secondary dark:bg-dark-secondary rounded-lg p-3">
<span className="font-medium text-black/70 dark:text-white/70">
Search query:
</span>{' '}
<div className="mb-4 text-sm bg-surface rounded-lg p-3">
<span className="font-medium opacity-70">Search query:</span>{' '}
{message.searchUrl ? (
<a
href={message.searchUrl}
target="_blank"
rel="noopener noreferrer"
className="dark:text-white text-black hover:underline"
className="hover:underline"
>
{message.searchQuery}
</a>
) : (
<span className="text-black dark:text-white">
{message.searchQuery}
</span>
<span>{message.searchQuery}</span>
)}
</div>
)}

View file

@ -159,7 +159,7 @@ const Navbar = ({
}, []);
return (
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm border-b bg-bg border-surface-2">
<a
href="/"
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden"
@ -174,7 +174,7 @@ const Navbar = ({
<div className="flex flex-row items-center space-x-4">
<Popover className="relative">
<PopoverButton className="active:scale-95 transition duration-100 cursor-pointer p-2 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary">
<PopoverButton className="active:scale-95 transition duration-100 cursor-pointer p-2 rounded-full hover:bg-surface-2">
<Share size={17} />
</PopoverButton>
<Transition
@ -186,20 +186,20 @@ const Navbar = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute right-0 mt-2 w-64 rounded-xl shadow-xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 z-50">
<PopoverPanel className="absolute right-0 mt-2 w-64 rounded-xl shadow-xl bg-surface border border-surface-2 z-50">
<div className="flex flex-col py-3 px-3 gap-2">
<button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-surface-2 transition-colors rounded-lg font-medium"
onClick={() => exportAsMarkdown(messages, title || '')}
>
<FileText size={17} className="text-[#24A0ED]" />
<FileText size={17} className="text-accent" />
Export as Markdown
</button>
<button
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-surface-2 transition-colors rounded-lg font-medium"
onClick={() => exportAsPDF(messages, title || '')}
>
<FileDown size={17} className="text-[#24A0ED]" />
<FileDown size={17} className="text-accent" />
Export as PDF
</button>
</div>

View file

@ -27,14 +27,14 @@ const NewsArticleWidget = () => {
}, []);
return (
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3 overflow-hidden">
<div className="bg-surface rounded-xl border border-surface-2 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3 overflow-hidden">
{loading ? (
<>
<div className="animate-pulse flex flex-row items-center w-full h-full">
<div className="rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 bg-light-200 dark:bg-dark-200 mr-3" />
<div className="rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 bg-surface-2 mr-3" />
<div className="flex flex-col justify-center flex-1 h-full w-0 gap-2">
<div className="h-4 w-3/4 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-4 w-3/4 rounded bg-surface-2" />
<div className="h-3 w-1/2 rounded bg-surface-2" />
</div>
</div>
</>
@ -46,7 +46,7 @@ const NewsArticleWidget = () => {
className="flex flex-row items-center w-full h-full group"
>
<img
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition"
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-surface-2 bg-surface-2 group-hover:opacity-90 transition"
src={
new URL(article.thumbnail).origin +
new URL(article.thumbnail).pathname +
@ -55,10 +55,10 @@ const NewsArticleWidget = () => {
alt={article.title}
/>
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0">
<div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap">
<div className="font-bold text-xs text-fg leading-tight truncate overflow-hidden whitespace-nowrap">
{article.title}
</div>
<p className="text-black/70 dark:text-white/70 text-xs leading-snug truncate overflow-hidden whitespace-nowrap">
<p className="text-fg/70 text-xs leading-snug truncate overflow-hidden whitespace-nowrap">
{article.content}
</p>
</div>

View file

@ -126,7 +126,7 @@ const SearchImages = ({
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
className="bg-surface-2 h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
/>
))}
</div>
@ -158,7 +158,7 @@ const SearchImages = ({
<div className="flex justify-center mt-4">
<button
onClick={handleShowMore}
className="px-4 py-2 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white rounded-md transition duration-200 flex items-center space-x-2"
className="px-4 py-2 bg-surface hover:bg-surface-2 text-fg/70 hover:text-fg rounded-md transition duration-200 flex items-center space-x-2 border border-surface-2"
>
<span>Show More Images</span>
<span className="text-sm opacity-75">

View file

@ -144,7 +144,7 @@ const Searchvideos = ({
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
className="bg-surface-2 h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
/>
))}
</div>
@ -173,7 +173,7 @@ const Searchvideos = ({
alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg"
/>
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<div className="absolute bg-bg/70 text-fg/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
<PlayCircle size={15} />
<p className="text-xs">Video</p>
</div>
@ -184,7 +184,7 @@ const Searchvideos = ({
<div className="flex justify-center mt-4">
<button
onClick={handleShowMore}
className="px-4 py-2 bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white rounded-md transition duration-200 flex items-center space-x-2"
className="px-4 py-2 bg-surface hover:bg-surface-2 text-fg/70 hover:text-fg rounded-md transition duration-200 flex items-center space-x-2 border border-surface-2"
>
<span>Show More Videos</span>
<span className="text-sm opacity-75">

View file

@ -53,7 +53,7 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col">
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-light-secondary dark:bg-dark-secondary px-2 py-8">
<div className="flex grow flex-col items-center justify-between gap-y-5 overflow-y-auto bg-surface px-2 py-8">
<a href="/">
<SquarePen className="cursor-pointer" />
</a>
@ -63,15 +63,13 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
key={i}
href={link.href}
className={cn(
'relative flex flex-row items-center justify-center cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 duration-150 transition w-full py-2 rounded-lg',
link.active
? 'text-black dark:text-white'
: 'text-black/70 dark:text-white/70',
'relative flex flex-row items-center justify-center cursor-pointer hover:bg-surface-2 duration-150 transition w-full py-2 rounded-lg',
link.active ? 'text-fg' : 'text-fg/70',
)}
>
<link.icon />
{link.active && (
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-black dark:bg-white" />
<div className="absolute right-0 -mr-2 h-full w-1 rounded-l-lg bg-accent" />
)}
</Link>
))}
@ -83,20 +81,18 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
</div>
</div>
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-light-primary dark:bg-dark-primary px-4 py-4 shadow-sm lg:hidden">
<div className="fixed bottom-0 w-full z-50 flex flex-row items-center gap-x-6 bg-bg px-4 py-4 shadow-sm lg:hidden">
{navLinks.map((link, i) => (
<Link
href={link.href}
key={i}
className={cn(
'relative flex flex-col items-center space-y-1 text-center w-full',
link.active
? 'text-black dark:text-white'
: 'text-black dark:text-white/70',
link.active ? 'text-fg' : 'text-fg/70',
)}
>
{link.active && (
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-black dark:bg-white" />
<div className="absolute top-0 -mt-4 h-1 w-full rounded-b-lg bg-accent" />
)}
<link.icon />
<p className="text-xs">{link.label}</p>

View file

@ -24,27 +24,24 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
onToggle || (() => setInternalExpanded(!internalExpanded));
return (
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">
<div className="my-4 bg-surface/50 rounded-xl border border-surface-2 overflow-hidden">
<button
onClick={handleToggle}
className="w-full flex items-center justify-between px-4 py-4 text-black/90 dark:text-white/90 hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200"
className="w-full flex items-center justify-between px-4 py-4 text-fg/90 hover:bg-surface-2 transition duration-200"
>
<div className="flex items-center space-x-2">
<BrainCircuit
size={20}
className="text-[#9C27B0] dark:text-[#CE93D8]"
/>
<BrainCircuit size={20} className="text-[#9C27B0]" />
<span className="font-medium text-sm">Thinking Process</span>
</div>
{isExpanded ? (
<ChevronUp size={18} className="text-black/70 dark:text-white/70" />
<ChevronUp size={18} className="text-fg/70" />
) : (
<ChevronDown size={18} className="text-black/70 dark:text-white/70" />
<ChevronDown size={18} className="text-fg/70" />
)}
</button>
{isExpanded && (
<div className="px-4 py-3 text-black/80 dark:text-white/80 text-sm border-t border-light-200 dark:border-dark-200 bg-light-100/50 dark:bg-dark-100/50 whitespace-pre-wrap">
<div className="px-4 py-3 text-fg/80 text-sm border-t border-surface-2 bg-surface/50 whitespace-pre-wrap">
{content}
</div>
)}

View file

@ -103,22 +103,22 @@ const WeatherWidget = () => {
}, []);
return (
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
<div className="bg-surface rounded-xl border border-surface-2 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
{loading ? (
<>
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full animate-pulse">
<div className="h-10 w-10 rounded-full bg-light-200 dark:bg-dark-200 mb-2" />
<div className="h-4 w-10 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-10 w-10 rounded-full bg-surface-2 mb-2" />
<div className="h-4 w-10 rounded bg-surface-2" />
</div>
<div className="flex flex-col justify-between flex-1 h-full py-1 animate-pulse">
<div className="flex flex-row items-center justify-between">
<div className="h-3 w-20 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-12 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-20 rounded bg-surface-2" />
<div className="h-3 w-12 rounded bg-surface-2" />
</div>
<div className="h-3 w-16 rounded bg-light-200 dark:bg-dark-200 mt-1" />
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200">
<div className="h-3 w-16 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-8 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-16 rounded bg-surface-2 mt-1" />
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-surface-2">
<div className="h-3 w-16 rounded bg-surface-2" />
<div className="h-3 w-8 rounded bg-surface-2" />
</div>
</div>
</>
@ -130,24 +130,22 @@ const WeatherWidget = () => {
alt={data.condition}
className="h-10 w-auto"
/>
<span className="text-base font-semibold text-black dark:text-white">
<span className="text-base font-semibold text-fg">
{data.temperature}°{data.temperatureUnit}
</span>
</div>
<div className="flex flex-col justify-between flex-1 h-full py-1">
<div className="flex flex-row items-center justify-between">
<span className="text-xs font-medium text-black dark:text-white">
<span className="text-xs font-medium text-fg">
{data.location}
</span>
<span className="flex items-center text-xs text-black/60 dark:text-white/60">
<span className="flex items-center text-xs text-fg/60">
<Wind className="w-3 h-3 mr-1" />
{data.windSpeed} {data.windSpeedUnit}
</span>
</div>
<span className="text-xs text-black/60 dark:text-white/60 mt-1">
{data.condition}
</span>
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60">
<span className="text-xs text-fg/60 mt-1">{data.condition}</span>
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-surface-2 text-xs text-fg/60">
<span>Humidity: {data.humidity}%</span>
<span>Now</span>
</div>

View file

@ -222,7 +222,7 @@ const WidgetConfigModal = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-75" />
<div className="fixed inset-0 bg-fg/75" />
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
@ -236,15 +236,15 @@ const WidgetConfigModal = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="w-full lg:max-w-[85vw] transform overflow-hidden rounded-2xl bg-light-primary dark:bg-dark-primary p-6 text-left align-middle shadow-xl transition-all">
<DialogPanel className="w-full lg:max-w-[85vw] transform overflow-hidden rounded-2xl bg-surface p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle
as="h3"
className="text-lg font-medium leading-6 text-black dark:text-white flex items-center justify-between"
className="text-lg font-medium leading-6 text-fg flex items-center justify-between"
>
{editingWidget ? 'Edit Widget' : 'Create New Widget'}
<button
onClick={handleClose}
className="p-1 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded"
className="p-1 hover:bg-surface-2 rounded"
>
<X size={20} />
</button>
@ -255,7 +255,7 @@ const WidgetConfigModal = ({
<div className="space-y-4">
{/* Widget Title */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-1">
<label className="block text-sm font-medium text-fg mb-1">
Widget Title
</label>
<input
@ -267,14 +267,14 @@ const WidgetConfigModal = ({
title: e.target.value,
}))
}
className="w-full px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
placeholder="Enter widget title..."
/>
</div>
{/* Source URLs */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-1">
<label className="block text-sm font-medium text-fg mb-1">
Source URLs
</label>
<div className="space-y-2">
@ -286,7 +286,7 @@ const WidgetConfigModal = ({
onChange={(e) =>
updateSource(index, 'url', e.target.value)
}
className="flex-1 px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
placeholder="https://example.com"
/>
<select
@ -298,7 +298,7 @@ const WidgetConfigModal = ({
e.target.value as Source['type'],
)
}
className="px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="Web Page">Web Page</option>
<option value="HTTP Data">HTTP Data</option>
@ -306,7 +306,7 @@ const WidgetConfigModal = ({
{config.sources.length > 1 && (
<button
onClick={() => removeSource(index)}
className="p-2 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
className="p-2 text-red-500 hover:bg-red-50 rounded"
>
<Trash2 size={16} />
</button>
@ -315,7 +315,7 @@ const WidgetConfigModal = ({
))}
<button
onClick={addSource}
className="flex items-center gap-2 px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
className="flex items-center gap-2 px-3 py-2 text-sm text-accent hover:bg-surface-2 rounded"
>
<Plus size={16} />
Add Source
@ -325,7 +325,7 @@ const WidgetConfigModal = ({
{/* LLM Prompt */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-1">
<label className="block text-sm font-medium text-fg mb-1">
LLM Prompt
</label>
<textarea
@ -337,14 +337,14 @@ const WidgetConfigModal = ({
}))
}
rows={8}
className="w-full px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
placeholder="Enter your prompt here..."
/>
</div>
{/* Provider and Model Selection */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-2">
<label className="block text-sm font-medium text-fg mb-2">
Model & Provider
</label>
<ModelSelector
@ -353,7 +353,7 @@ const WidgetConfigModal = ({
truncateModelName={false}
showModelName={true}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-fg/60 mt-1">
Select the AI model and provider to process your widget
content
</p>
@ -361,14 +361,14 @@ const WidgetConfigModal = ({
{/* Tool Selection */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-2">
<label className="block text-sm font-medium text-fg mb-2">
Available Tools
</label>
<ToolSelector
selectedToolNames={selectedTools}
onSelectedToolNamesChange={setSelectedTools}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-fg/60 mt-1">
Select tools to assist the AI in processing your widget.
Your model must support tool calling.
</p>
@ -376,7 +376,7 @@ const WidgetConfigModal = ({
{/* Refresh Frequency */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-1">
<label className="block text-sm font-medium text-fg mb-1">
Refresh Frequency
</label>
<div className="flex gap-2">
@ -390,7 +390,7 @@ const WidgetConfigModal = ({
refreshFrequency: parseInt(e.target.value) || 1,
}))
}
className="flex-1 px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
/>
<select
value={config.refreshUnit}
@ -402,7 +402,7 @@ const WidgetConfigModal = ({
| 'hours',
}))
}
className="px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 border border-surface-2 rounded-md bg-bg text-fg focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="minutes">Minutes</option>
<option value="hours">Hours</option>
@ -414,29 +414,22 @@ const WidgetConfigModal = ({
{/* Right Column - Preview */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-black dark:text-white">
Preview
</h4>
<h4 className="text-sm font-medium text-fg">Preview</h4>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Brain
size={16}
className="text-gray-600 dark:text-gray-400"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">
Thinking
</span>
<Brain size={16} className="text-fg/70" />
<span className="text-sm text-fg/80">Thinking</span>
<Switch
checked={showThinking}
onChange={setShowThinking}
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
className="bg-surface border border-surface-2 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
>
<span className="sr-only">Show thinking tags</span>
<span
className={`${
showThinking
? 'translate-x-6 bg-purple-600'
: 'translate-x-1 bg-black/50 dark:bg-white/50'
: 'translate-x-1 bg-fg/50'
} inline-block h-3 w-3 sm:h-4 sm:w-4 transform rounded-full transition-all duration-200`}
/>
</Switch>
@ -444,7 +437,7 @@ const WidgetConfigModal = ({
<button
onClick={handlePreview}
disabled={isPreviewLoading}
className="flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
className="flex items-center gap-2 px-3 py-2 bg-accent text-white rounded hover:bg-accent-700 disabled:opacity-50"
>
<Play size={16} />
{isPreviewLoading ? 'Loading...' : 'Run Preview'}
@ -452,16 +445,16 @@ const WidgetConfigModal = ({
</div>
</div>
<div className="h-80 p-4 border border-light-200 dark:border-dark-200 rounded-md bg-light-secondary dark:bg-dark-secondary overflow-y-auto max-w-full">
<div className="h-80 p-4 border border-surface-2 rounded-md bg-surface overflow-y-auto max-w-full">
{previewContent ? (
<div className="prose prose-sm dark:prose-invert max-w-full">
<div className="prose prose-sm max-w-full">
<MarkdownRenderer
showThinking={showThinking}
content={previewContent}
/>
</div>
) : (
<div className="text-sm text-black/50 dark:text-white/50 italic">
<div className="text-sm text-fg/50 italic">
Click &quot;Run Preview&quot; to see how your widget
will look
</div>
@ -469,41 +462,41 @@ const WidgetConfigModal = ({
</div>
{/* Variable Legend */}
<div className="text-xs text-black/70 dark:text-white/70">
<div className="text-xs text-fg/70">
<h5 className="font-medium mb-2">Available Variables:</h5>
<div className="space-y-1">
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{current_utc_datetime}}'}
</code>{' '}
- Current UTC date and time
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{current_local_datetime}}'}
</code>{' '}
- Current local date and time
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{source_content_1}}'}
</code>{' '}
- Content from first source
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{source_content_2}}'}
</code>{' '}
- Content from second source
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{source_content_...}}'}
</code>{' '}
- Content from nth source
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
<code className="bg-surface-2 px-1 rounded">
{'{{location}}'}
</code>{' '}
- Your current location
@ -517,13 +510,13 @@ const WidgetConfigModal = ({
<div className="mt-6 flex justify-end gap-3">
<button
onClick={handleClose}
className="px-4 py-2 text-sm font-medium text-black dark:text-white bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 rounded-md"
className="px-4 py-2 text-sm font-medium text-fg bg-surface hover:bg-surface-2 rounded-md"
>
Cancel
</button>
<button
onClick={handleSave}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md"
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent hover:bg-accent-700 rounded-md"
>
<Save size={16} />
{editingWidget ? 'Update Widget' : 'Create Widget'}

View file

@ -10,6 +10,7 @@ import {
ChevronUp,
GripVertical,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import MarkdownRenderer from '@/components/MarkdownRenderer';
import { Widget } from '@/lib/types/widget';
@ -56,13 +57,10 @@ const WidgetDisplay = ({
<div className="flex items-center space-x-2 flex-1 min-w-0">
{/* Drag Handle */}
<div
className="widget-drag-handle flex-shrink-0 p-1 rounded hover:bg-light-secondary dark:hover:bg-dark-secondary cursor-move transition-colors"
className="widget-drag-handle flex-shrink-0 p-1 rounded hover:bg-surface-2 cursor-move transition-colors"
title="Drag to move widget"
>
<GripVertical
size={16}
className="text-gray-400 dark:text-gray-500"
/>
<GripVertical size={16} className="text-fg/50" />
</div>
<CardTitle className="text-lg font-medium truncate">
@ -73,7 +71,7 @@ const WidgetDisplay = ({
<div className="flex items-center space-x-2 flex-shrink-0">
{/* Last updated date with refresh frequency tooltip */}
<span
className="text-xs text-gray-500 dark:text-gray-400"
className="text-xs text-fg/60"
title={getRefreshFrequencyText()}
>
{formatLastUpdated(widget.lastUpdated)}
@ -83,12 +81,15 @@ const WidgetDisplay = ({
<button
onClick={() => onRefresh(widget.id)}
disabled={widget.isLoading}
className="p-1.5 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded transition-colors disabled:opacity-50"
className="p-1.5 hover:bg-surface-2 rounded transition-colors disabled:opacity-50"
title="Refresh Widget"
>
<RefreshCw
size={16}
className={`text-gray-600 dark:text-gray-400 ${widget.isLoading ? 'animate-spin' : ''}`}
className={cn(
'text-fg/70',
widget.isLoading ? 'animate-spin' : '',
)}
/>
</button>
</div>
@ -98,31 +99,29 @@ const WidgetDisplay = ({
<CardContent className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
{widget.isLoading ? (
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-center py-8 text-fg/60">
<RefreshCw size={20} className="animate-spin mr-2" />
<span>Loading content...</span>
</div>
) : widget.error ? (
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
<div className="flex items-start space-x-2 p-3 bg-red-50 rounded border border-red-200">
<AlertCircle
size={16}
className="text-red-500 mt-0.5 flex-shrink-0"
/>
<div className="flex-1">
<p className="text-sm font-medium text-red-800 dark:text-red-300">
<p className="text-sm font-medium text-red-800">
Error Loading Content
</p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
{widget.error}
</p>
<p className="text-xs text-red-600 mt-1">{widget.error}</p>
</div>
</div>
) : widget.content ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<div className="prose prose-sm max-w-none">
<MarkdownRenderer content={widget.content} showThinking={false} />
</div>
) : (
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-center py-8 text-fg/60">
<div className="text-center">
<p className="text-sm">No content yet</p>
<p className="text-xs mt-1">Click refresh to load content</p>
@ -133,10 +132,10 @@ const WidgetDisplay = ({
</CardContent>
{/* Collapsible footer with sources and actions */}
<div className="bg-light-secondary/30 dark:bg-dark-secondary/30 flex-shrink-0">
<div className="bg-surface/30 flex-shrink-0">
<button
onClick={() => setIsFooterExpanded(!isFooterExpanded)}
className="w-full px-4 py-2 flex items-center space-x-2 text-xs text-gray-500 dark:text-gray-400 hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors"
className="w-full px-4 py-2 flex items-center space-x-2 text-xs text-fg/60 hover:bg-surface-2 transition-colors"
>
{isFooterExpanded ? (
<ChevronUp size={14} />
@ -151,22 +150,16 @@ const WidgetDisplay = ({
{/* Sources */}
{widget.sources.length > 0 && (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
Sources:
</p>
<p className="text-xs text-fg/60 mb-2">Sources:</p>
<div className="space-y-1">
{widget.sources.map((source, index) => (
<div
key={index}
className="flex items-center space-x-2 text-xs"
>
<span className="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
<span className="text-gray-600 dark:text-gray-300 truncate">
{source.url}
</span>
<span className="text-gray-400 dark:text-gray-500">
({source.type})
</span>
<span className="inline-block w-2 h-2 bg-accent rounded-full"></span>
<span className="text-fg/70 truncate">{source.url}</span>
<span className="text-fg/60">({source.type})</span>
</div>
))}
</div>
@ -177,7 +170,7 @@ const WidgetDisplay = ({
<div className="flex items-center space-x-2 pt-2">
<button
onClick={() => onEdit(widget)}
className="flex items-center space-x-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-400 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded transition-colors"
className="flex items-center space-x-1 px-2 py-1 text-xs text-fg/70 hover:bg-surface-2 rounded transition-colors"
>
<Edit size={12} />
<span>Edit</span>
@ -185,7 +178,7 @@ const WidgetDisplay = ({
<button
onClick={() => onDelete(widget.id)}
className="flex items-center space-x-1 px-2 py-1 text-xs text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
className="flex items-center space-x-1 px-2 py-1 text-xs text-red-500 hover:bg-surface-2 rounded transition-colors"
>
<Trash2 size={12} />
<span>Delete</span>

View file

@ -0,0 +1,209 @@
'use client';
import { useEffect, useState } from 'react';
export type AppTheme = 'light' | 'dark' | 'custom';
type Props = {
children: React.ReactNode;
};
export default function ThemeController({ children }: Props) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
const savedTheme = (localStorage.getItem('appTheme') as AppTheme) || 'dark';
const userBg = localStorage.getItem('userBg') || '';
const userAccent = localStorage.getItem('userAccent') || '';
applyTheme(savedTheme, userBg, userAccent);
}, []);
const applyTheme = (mode: AppTheme, bg?: string, accent?: string) => {
const root = document.documentElement;
root.setAttribute('data-theme', mode);
if (mode === 'custom') {
if (bg) {
root.style.setProperty('--color-bg', normalizeColor(bg));
// decide foreground based on luminance
const luminance = getLuminance(bg);
root.style.setProperty(
'--color-fg',
luminance > 0.5 ? '#000000' : '#ffffff',
);
// surfaces
const surface = adjustLightness(bg, luminance > 0.5 ? -0.06 : 0.08);
const surface2 = adjustLightness(bg, luminance > 0.5 ? -0.1 : 0.12);
root.style.setProperty('--color-surface', surface);
root.style.setProperty('--color-surface-2', surface2);
root.classList.toggle('dark', luminance <= 0.5);
}
if (accent) {
const a600 = normalizeColor(accent);
const a700 = adjustLightness(a600, -0.1);
const a500 = adjustLightness(a600, 0.1);
root.style.setProperty('--color-accent-600', a600);
root.style.setProperty('--color-accent-700', a700);
root.style.setProperty('--color-accent-500', a500);
root.style.setProperty('--color-accent', a600);
// Map default blue to accent to minimize code changes
root.style.setProperty('--color-blue-600', a600);
root.style.setProperty('--color-blue-700', a700);
root.style.setProperty('--color-blue-500', a500);
root.style.setProperty('--color-blue-50', adjustLightness(a600, 0.92));
root.style.setProperty('--color-blue-900', adjustLightness(a600, -0.4));
}
} else {
// Clear any inline custom overrides so stylesheet tokens take effect
const toClear = [
'--user-bg',
'--user-accent',
'--color-bg',
'--color-fg',
'--color-surface',
'--color-surface-2',
'--color-accent-600',
'--color-accent-700',
'--color-accent-500',
'--color-accent',
'--color-blue-600',
'--color-blue-700',
'--color-blue-500',
'--color-blue-50',
'--color-blue-900',
];
toClear.forEach((name) => root.style.removeProperty(name));
root.classList.toggle('dark', mode === 'dark');
}
};
useEffect(() => {
(window as any).__setAppTheme = (
mode: AppTheme,
bg?: string,
accent?: string,
) => {
localStorage.setItem('appTheme', mode);
if (mode === 'custom') {
if (bg) localStorage.setItem('userBg', bg);
if (accent) localStorage.setItem('userAccent', accent);
}
applyTheme(mode, bg, accent);
};
}, []);
if (!mounted) return null;
return <>{children}</>;
}
// helpers
function normalizeColor(c: string): string {
if (c.startsWith('#') && (c.length === 4 || c.length === 7)) return c;
try {
// Attempt to parse rgb(...) or other; create a canvas to normalize
const ctx = document.createElement('canvas').getContext('2d');
if (!ctx) return c;
ctx.fillStyle = c;
const v = ctx.fillStyle as string;
// convert to hex
const m = v.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (m) {
const r = Number(m[1]),
g = Number(m[2]),
b = Number(m[3]);
return rgbToHex(r, g, b);
}
return c;
} catch {
return c;
}
}
function getLuminance(hex: string): number {
const { r, g, b } = hexToRgb(hex);
const [R, G, B] = [r, g, b].map((v) => {
const srgb = v / 255;
return srgb <= 0.03928
? srgb / 12.92
: Math.pow((srgb + 0.055) / 1.055, 2.4);
});
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function adjustLightness(hex: string, delta: number): string {
// delta in [-1, 1] add to perceived lightness roughly
const { r, g, b } = hexToRgb(hex);
// convert to HSL
let { h, s, l } = rgbToHsl(r, g, b);
l = Math.max(0, Math.min(1, l + delta));
const { r: nr, g: ng, b: nb } = hslToRgb(h, s, l);
return rgbToHex(nr, ng, nb);
}
function hexToRgb(hex: string): { r: number; g: number; b: number } {
let h = hex.replace('#', '');
if (h.length === 3)
h = h
.split('')
.map((c) => c + c)
.join('');
const num = parseInt(h, 16);
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
}
function rgbToHex(r: number, g: number, b: number): string {
return '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
}
function rgbToHsl(r: number, g: number, b: number) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h = 0,
s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h, s, l };
}
function hslToRgb(h: number, s: number, l: number) {
let r: number, g: number, b: number;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}

View file

@ -1,59 +1,76 @@
'use client';
import { useTheme } from 'next-themes';
import { useCallback, useEffect, useState } from 'react';
import Select from '../ui/Select';
import { useEffect, useState } from 'react';
type Theme = 'dark' | 'light' | 'system';
type Theme = 'dark' | 'light' | 'custom';
const ThemeSwitcher = ({ className }: { className?: string }) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
const isTheme = useCallback((t: Theme) => t === theme, [theme]);
const handleThemeSwitch = (theme: Theme) => {
setTheme(theme);
};
const [theme, setTheme] = useState<Theme>('dark');
const [bg, setBg] = useState<string>('');
const [accent, setAccent] = useState<string>('');
useEffect(() => {
setMounted(true);
const t = (localStorage.getItem('appTheme') as Theme) || 'dark';
const b = localStorage.getItem('userBg') || '#0f0f0f';
const a = localStorage.getItem('userAccent') || '#2563eb';
setTheme(t);
setBg(b);
setAccent(a);
}, []);
useEffect(() => {
if (isTheme('system')) {
const preferDarkScheme = window.matchMedia(
'(prefers-color-scheme: dark)',
);
const detectThemeChange = (event: MediaQueryListEvent) => {
const theme: Theme = event.matches ? 'dark' : 'light';
setTheme(theme);
};
preferDarkScheme.addEventListener('change', detectThemeChange);
return () => {
preferDarkScheme.removeEventListener('change', detectThemeChange);
};
const apply = (next: Theme, nextBg = bg, nextAccent = accent) => {
(window as any).__setAppTheme?.(next, nextBg, nextAccent);
setTheme(next);
if (next === 'light' || next === 'dark') {
// Refresh local color inputs from storage so UI shows current defaults
const b = localStorage.getItem('userBg') || '#0f0f0f';
const a = localStorage.getItem('userAccent') || '#2563eb';
setBg(b);
setAccent(a);
}
}, [isTheme, setTheme, theme]);
};
// Avoid Hydration Mismatch
if (!mounted) {
return null;
}
if (!mounted) return null;
return (
<Select
className={className}
value={theme}
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
]}
/>
<div className={className}>
<div className="flex gap-2">
<select
className="bg-surface text-fg px-3 py-2 rounded-lg border border-surface-2 text-sm"
value={theme}
onChange={(e) => apply(e.target.value as Theme)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="custom">Custom</option>
</select>
{theme === 'custom' && (
<div className="flex items-center gap-2">
<label className="text-xs text-foreground/70">Background</label>
<input
type="color"
value={bg}
onChange={(e) => {
const v = e.target.value;
setBg(v);
apply('custom', v, accent);
}}
/>
<label className="text-xs text-foreground/70">Accent</label>
<input
type="color"
value={accent}
onChange={(e) => {
const v = e.target.value;
setAccent(v);
apply('custom', bg, v);
}}
/>
</div>
)}
</div>
</div>
);
};

View file

@ -10,7 +10,7 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
<select
{...restProps}
className={cn(
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm',
'bg-surface px-3 py-2 flex items-center overflow-hidden border border-surface-2 text-fg rounded-lg text-sm',
className,
)}
>

View file

@ -322,7 +322,8 @@ Your task is to provide answers that are:
- This query will be passed directly to the search engine
- You will receive a list of relevant documents containing snippets of the web page, a URL, and the title of the web page
- Always perform at least one web search unless the question can be definitively answered with previous conversation history or local file content
${fileIds.length > 0
${
fileIds.length > 0
? `
2.1. **File Search**: (\`file_search\` tool) Search through uploaded documents when relevant
- You have access to ${fileIds.length} uploaded file${fileIds.length === 1 ? '' : 's'} that may contain relevant information
@ -331,8 +332,8 @@ Your task is to provide answers that are:
- The tool will automatically search through all available uploaded files
- Focus your file searches on specific aspects of the user's query that might be covered in the uploaded documents
- **Important**: You do NOT need to specify file IDs - the tool will automatically search through all available uploaded files.`
: ''
}
: ''
}
3. **Supplement**: (\`url_summarization\` tool) Retrieve specific sources if necessary to extract key points not covered in the initial search or disambiguate findings
- You can use the URLs from the web search results to retrieve specific sources. They must be passed to the tool unchanged
- URLs can be passed as an array to request multiple sources at once

View file

@ -0,0 +1,213 @@
/**
* Color utility functions for theme calculations and accessibility
* Based on WCAG 2.1 contrast ratio guidelines
*/
/**
* Converts hex color to RGB values
* @param hex - Hex color string (e.g., '#ff0000' or '#f00')
* @returns RGB object with r, g, b values (0-255)
*/
export function hexToRgb(
hex: string,
): { r: number; g: number; b: number } | null {
// Remove the hash if present
hex = hex.replace('#', '');
// Convert 3-digit hex to 6-digit
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char + char)
.join('');
}
if (hex.length !== 6) {
return null;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b };
}
/**
* Converts RGB values to hex color
* @param r - Red value (0-255)
* @param g - Green value (0-255)
* @param b - Blue value (0-255)
* @returns Hex color string
*/
export function rgbToHex(r: number, g: number, b: number): string {
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}
/**
* Converts sRGB color component to linear RGB
* @param colorComponent - Color component (0-255)
* @returns Linear RGB component (0-1)
*/
function sRGBToLinear(colorComponent: number): number {
const c = colorComponent / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
/**
* Calculates the relative luminance of a color according to WCAG 2.1
* @param hex - Hex color string
* @returns Relative luminance (0-1)
*/
export function calculateLuminance(hex: string): number {
const rgb = hexToRgb(hex);
if (!rgb) return 0;
const { r, g, b } = rgb;
// Convert to linear RGB
const linearR = sRGBToLinear(r);
const linearG = sRGBToLinear(g);
const linearB = sRGBToLinear(b);
// Calculate relative luminance using WCAG formula
return 0.2126 * linearR + 0.7152 * linearG + 0.0722 * linearB;
}
/**
* Calculates the contrast ratio between two colors according to WCAG 2.1
* @param color1 - First hex color
* @param color2 - Second hex color
* @returns Contrast ratio (1-21)
*/
export function calculateContrastRatio(color1: string, color2: string): number {
const luminance1 = calculateLuminance(color1);
const luminance2 = calculateLuminance(color2);
const lighter = Math.max(luminance1, luminance2);
const darker = Math.min(luminance1, luminance2);
return (lighter + 0.05) / (darker + 0.05);
}
/**
* Determines if a color is considered "light" (high luminance)
* @param hex - Hex color string
* @returns true if color is light
*/
export function isLightColor(hex: string): boolean {
return calculateLuminance(hex) > 0.5;
}
/**
* Gets appropriate text color (black or white) for maximum contrast
* @param backgroundHex - Background hex color
* @returns '#000000' for light backgrounds, '#ffffff' for dark backgrounds
*/
export function getContrastingTextColor(backgroundHex: string): string {
return isLightColor(backgroundHex) ? '#000000' : '#ffffff';
}
/**
* Checks if color combination meets WCAG contrast requirements
* @param foregroundHex - Foreground color hex
* @param backgroundHex - Background color hex
* @param level - WCAG level ('AA' | 'AAA')
* @param size - Text size ('normal' | 'large')
* @returns true if contrast ratio is sufficient
*/
export function meetsContrastRequirement(
foregroundHex: string,
backgroundHex: string,
level: 'AA' | 'AAA' = 'AA',
size: 'normal' | 'large' = 'normal',
): boolean {
const contrastRatio = calculateContrastRatio(foregroundHex, backgroundHex);
// WCAG 2.1 contrast requirements
const requirements = {
AA: { normal: 4.5, large: 3.0 },
AAA: { normal: 7.0, large: 4.5 },
};
return contrastRatio >= requirements[level][size];
}
/**
* Adjusts color brightness by a percentage
* @param hex - Hex color string
* @param percent - Brightness adjustment percentage (-100 to 100)
* @returns Adjusted hex color
*/
export function adjustBrightness(hex: string, percent: number): string {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
const adjust = (color: number): number => {
const adjusted = color + (percent / 100) * 255;
return Math.max(0, Math.min(255, Math.round(adjusted)));
};
return rgbToHex(adjust(rgb.r), adjust(rgb.g), adjust(rgb.b));
}
/**
* Creates a hover variant of a color (slightly darker/lighter)
* @param hex - Base hex color
* @param amount - Adjustment amount (default: 10% darker for light colors, 15% lighter for dark)
* @returns Hover variant hex color
*/
export function createHoverVariant(hex: string, amount?: number): string {
const defaultAmount = isLightColor(hex) ? -10 : 15;
return adjustBrightness(hex, amount ?? defaultAmount);
}
/**
* Creates a secondary variant of a color (more subtle)
* @param hex - Base hex color
* @param opacity - Opacity factor (0-1, default: 0.1)
* @returns Secondary variant hex color (mixed with appropriate base)
*/
export function createSecondaryVariant(
hex: string,
opacity: number = 0.1,
): string {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
// For light colors, mix with black to make slightly darker
// For dark colors, mix with white to make slightly lighter
const base = isLightColor(hex) ? 0 : 255;
const mix = (color: number): number => {
return Math.round(color * (1 - opacity) + base * opacity);
};
return rgbToHex(mix(rgb.r), mix(rgb.g), mix(rgb.b));
}
/**
* Validates and normalizes hex color format
* @param hex - Input hex color (with or without #)
* @returns Normalized hex color string or null if invalid
*/
export function normalizeHexColor(hex: string): string | null {
// Remove any whitespace
hex = hex.trim();
// Add # if missing
if (!hex.startsWith('#')) {
hex = '#' + hex;
}
// Convert 3-digit to 6-digit
if (hex.length === 4) {
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
}
// Validate final format
if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) {
return null;
}
return hex.toLowerCase();
}

View file

@ -1,52 +1,3 @@
import type { Config } from 'tailwindcss';
import type { DefaultColors } from 'tailwindcss/types/generated/colors';
const themeDark = (colors: DefaultColors) => ({
50: '#0a0a0a',
100: '#111111',
200: '#1c1c1c',
});
const themeLight = (colors: DefaultColors) => ({
50: '#fcfcf9',
100: '#f3f3ee',
200: '#e8e8e3',
});
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
borderColor: ({ colors }) => {
return {
light: themeLight(colors),
dark: themeDark(colors),
};
},
colors: ({ colors }) => {
const colorsDark = themeDark(colors);
const colorsLight = themeLight(colors);
return {
dark: {
primary: colorsDark[50],
secondary: colorsDark[100],
...colorsDark,
},
light: {
primary: colorsLight[50],
secondary: colorsLight[100],
...colorsLight,
},
};
},
},
},
plugins: [require('@tailwindcss/typography')],
};
export default config;
// Tailwind v4 uses CSS-first configuration via @theme in globals.css.
// Keeping an empty config to satisfy tooling that expects this file.
export default {};

745
yarn.lock

File diff suppressed because it is too large Load diff