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 - `/langchain-ai/langgraph` for LangGraph
- `/quantizor/markdown-to-jsx` for Markdown to JSX conversion - `/quantizor/markdown-to-jsx` for Markdown to JSX conversion
- `/context7/headlessui_com` for Headless UI components - `/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" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/better-sqlite3": "^7.6.12", "@types/better-sqlite3": "^7.6.12",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
@ -77,7 +78,7 @@
"eslint-config-next": "14.1.4", "eslint-config-next": "14.1.4",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"tailwindcss": "^3.3.0", "tailwindcss": "^4.0.0",
"typescript": "^5" "typescript": "^5"
} }
} }

View file

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

View file

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

View file

@ -50,7 +50,7 @@ const Page = () => {
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<svg <svg
aria-hidden="true" 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -73,7 +73,7 @@ const Page = () => {
<Search /> <Search />
<h1 className="text-3xl font-medium p-2">Discover</h1> <h1 className="text-3xl font-medium p-2">Discover</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> </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"> <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 <Link
href={`/?q=Summary: ${item.url}`} href={`/?q=Summary: ${item.url}`}
key={i} 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" target="_blank"
> >
<img <img
@ -95,10 +95,10 @@ const Page = () => {
alt={item.title} alt={item.title}
/> />
<div className="px-6 py-4"> <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)}... {item.title.slice(0, 100)}...
</div> </div>
<p className="text-black-70 dark:text-white/70 text-sm"> <p className="text-fg/70 text-sm">
{item.content.slice(0, 100)}... {item.content.slice(0, 100)}...
</p> </p>
</div> </div>

View file

@ -2,9 +2,36 @@
@import 'react-grid-layout/css/styles.css'; @import 'react-grid-layout/css/styles.css';
@import 'react-resizable/css/styles.css'; @import 'react-resizable/css/styles.css';
@tailwind base; @import 'tailwindcss';
@tailwind components;
@tailwind utilities; /* 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 { @layer base {
.overflow-hidden-scrollable { .overflow-hidden-scrollable {
@ -14,6 +41,39 @@
.overflow-hidden-scrollable::-webkit-scrollbar { .overflow-hidden-scrollable::-webkit-scrollbar {
display: none; 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 { @layer utilities {
@ -37,3 +97,55 @@
font-size: 16px !important; 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 { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar'; import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider'; import ThemeController from '@/components/theme/Controller';
const montserrat = Montserrat({ const montserrat = Montserrat({
weight: ['300', '400', '500', '700'], weight: ['300', '400', '500', '700'],
@ -25,7 +25,12 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html className="h-full" lang="en" suppressHydrationWarning> <html
className="h-full dark"
lang="en"
suppressHydrationWarning
data-theme="dark"
>
<head> <head>
<link <link
rel="search" rel="search"
@ -34,19 +39,19 @@ export default function RootLayout({
href="/api/opensearch" href="/api/opensearch"
/> />
</head> </head>
<body className={cn('h-full', montserrat.className)}> <body className={cn('h-full bg-bg text-fg', montserrat.className)}>
<ThemeProvider> <ThemeController>
<Sidebar>{children}</Sidebar> <Sidebar>{children}</Sidebar>
<Toaster <Toaster
toastOptions={{ toastOptions={{
unstyled: true, unstyled: true,
classNames: { classNames: {
toast: 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> </body>
</html> </html>
); );

View file

@ -41,7 +41,7 @@ const Page = () => {
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<svg <svg
aria-hidden="true" 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -63,13 +63,11 @@ const Page = () => {
<BookOpenText /> <BookOpenText />
<h1 className="text-3xl font-medium p-2">Library</h1> <h1 className="text-3xl font-medium p-2">Library</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> </div>
{chats.length === 0 && ( {chats.length === 0 && (
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-fg/70 text-sm">No chats found.</p>
No chats found.
</p>
</div> </div>
)} )}
{chats.length > 0 && ( {chats.length > 0 && (
@ -78,20 +76,18 @@ const Page = () => {
<div <div
className={cn( className={cn(
'flex flex-col space-y-4 py-6', 'flex flex-col space-y-4 py-6',
i !== chats.length - 1 i !== chats.length - 1 ? 'border-b border-surface-2' : '',
? 'border-b border-white-200 dark:border-dark-200'
: '',
)} )}
key={i} key={i}
> >
<Link <Link
href={`/c/${chat.id}`} 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} {chat.title}
</Link> </Link>
<div className="flex flex-row items-center justify-between w-full"> <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} /> <ClockIcon size={15} />
<p className="text-xs"> <p className="text-xs">
{formatTimeDifference(new Date(), chat.createdAt)} Ago {formatTimeDifference(new Date(), chat.createdAt)} Ago

View file

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

View file

@ -232,7 +232,7 @@ const Chat = ({
onThinkBoxToggle={onThinkBoxToggle} onThinkBoxToggle={onThinkBoxToggle}
/> />
{!isLast && msg.role === 'assistant' && ( {!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> </Fragment>
); );
@ -248,7 +248,7 @@ const Chat = ({
setIsAtBottom(true); setIsAtBottom(true);
messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); 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" aria-label="Scroll to bottom"
> >
<svg <svg

View file

@ -812,7 +812,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
</Link> </Link>
</div> </div>
<div className="flex flex-col items-center justify-center min-h-screen"> <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. Failed to connect to the server. Please try again later.
</p> </p>
</div> </div>
@ -870,7 +870,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
<svg <svg
aria-hidden="true" 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -19,7 +19,7 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
href={url} href={url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} {number}
</a> </a>
@ -64,14 +64,14 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
transform: 'translate(-50%, -100%)', 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 <MessageSource
source={source} 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> </div>
{/* Tooltip arrow */} {/* 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>, </div>,
document.body, 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="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild <TransitionChild
@ -87,11 +87,11 @@ const DeleteChat = ({
leaveFrom="opacity-100 scale-200" leaveFrom="opacity-100 scale-200"
leaveTo="opacity-0 scale-95" 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"> <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 dark:text-white"> <DialogTitle className="text-lg font-medium leading-6">
Delete Confirmation Delete Confirmation
</DialogTitle> </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? Are you sure you want to delete this chat?
</Description> </Description>
<div className="flex flex-row items-end justify-end space-x-4 mt-6"> <div className="flex flex-row items-end justify-end space-x-4 mt-6">
@ -101,7 +101,7 @@ const DeleteChat = ({
setConfirmationDialogOpen(false); 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 Cancel
</button> </button>

View file

@ -40,9 +40,7 @@ const EmptyChat = ({
</div> </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 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"> <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"> <h2 className="text-3xl font-medium -mt-8">Research begins here.</h2>
Research begins here.
</h2>
<MessageInput <MessageInput
firstMessage={true} firstMessage={true}
loading={false} loading={false}

View file

@ -7,7 +7,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
const isDashboard = segments.includes('dashboard'); const isDashboard = segments.includes('dashboard');
return ( 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'}> <div className={isDashboard ? 'mx-4' : 'max-w-screen-lg lg:mx-auto mx-4'}>
{children} {children}
</div> </div>

View file

@ -11,13 +11,12 @@ import {
Settings, Settings,
} from 'lucide-react'; } from 'lucide-react';
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'; 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 { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { import {
oneDark, oneDark,
oneLight, oneLight,
} from 'react-syntax-highlighter/dist/cjs/styles/prism'; } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { useTheme } from 'next-themes';
import ThinkBox from './ThinkBox'; import ThinkBox from './ThinkBox';
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import CitationLink from './CitationLink'; import CitationLink from './CitationLink';
@ -81,23 +80,15 @@ const ToolCall = ({
switch (toolType) { switch (toolType) {
case 'search': case 'search':
case 'web_search': case 'web_search':
return ( return <Search size={16} className="text-accent" />;
<Search size={16} className="text-blue-600 dark:text-blue-400" />
);
case 'file': case 'file':
case 'file_search': case 'file_search':
return ( return <FileText size={16} className="text-green-600" />;
<FileText size={16} className="text-green-600 dark:text-green-400" />
);
case 'url': case 'url':
case 'url_summarization': case 'url_summarization':
return ( return <Globe size={16} className="text-purple-600" />;
<Globe size={16} className="text-purple-600 dark:text-purple-400" />
);
default: default:
return ( return <Settings size={16} className="text-fg/70" />;
<Settings size={16} className="text-gray-600 dark:text-gray-400" />
);
} }
}; };
@ -106,8 +97,8 @@ const ToolCall = ({
return ( return (
<> <>
<span className="mr-2">{getIcon(type)}</span> <span className="mr-2">{getIcon(type)}</span>
<span className="text-black/60 dark:text-white/60">Web search:</span> <span>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 className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm">
{query || children} {query || children}
</span> </span>
</> </>
@ -118,8 +109,8 @@ const ToolCall = ({
return ( return (
<> <>
<span className="mr-2">{getIcon(type)}</span> <span className="mr-2">{getIcon(type)}</span>
<span className="text-black/60 dark:text-white/60">File search:</span> <span>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 className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm">
{query || children} {query || children}
</span> </span>
</> </>
@ -131,7 +122,7 @@ const ToolCall = ({
return ( return (
<> <>
<span className="mr-2">{getIcon(type)}</span> <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 Analyzing {urlCount} web page{urlCount === 1 ? '' : 's'} for
additional details additional details
</span> </span>
@ -143,8 +134,8 @@ const ToolCall = ({
return ( return (
<> <>
<span className="mr-2">{getIcon(type || 'default')}</span> <span className="mr-2">{getIcon(type || 'default')}</span>
<span className="text-black/60 dark:text-white/60">Using tool:</span> <span>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 className="ml-2 px-2 py-0.5 bg-fg/5 rounded font-mono text-sm border border-surface-2">
{type || 'unknown'} {type || 'unknown'}
</span> </span>
</> </>
@ -152,7 +143,7 @@ const ToolCall = ({
}; };
return ( 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"> <div className="flex items-center text-sm font-medium">
{formatToolMessage()} {formatToolMessage()}
</div> </div>
@ -190,7 +181,24 @@ const CodeBlock = ({
className?: string; className?: string;
children: React.ReactNode; 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") // Extract language from className (format could be "language-javascript" or "lang-javascript")
let language = ''; let language = '';
@ -211,23 +219,23 @@ const CodeBlock = ({
setTimeout(() => setIsCopied(false), 2000); setTimeout(() => setIsCopied(false), 2000);
}; };
// Choose syntax highlighting style based on theme // Choose syntax highlighting style based on actual dark/light class
const syntaxStyle = theme === 'light' ? oneLight : oneDark; const syntaxStyle = isDark ? oneDark : oneLight;
const backgroundStyle = theme === 'light' ? '#fafafa' : '#1c1c1c'; const backgroundStyle = isDark ? '#1c1c1c' : '#fafafa';
return ( return (
<div className="rounded-md overflow-hidden my-4 relative group border border-light-200 dark:border-dark-secondary"> <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-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="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> <span>{language}</span>
<button <button
onClick={handleCopyCode} 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" aria-label="Copy code to clipboard"
> >
{isCopied ? ( {isCopied ? (
<CheckCheck size={14} className="text-green-500" /> <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> </button>
</div> </div>
@ -312,7 +320,7 @@ const MarkdownRenderer = ({
} }
// This is an inline code block (`code`) // This is an inline code block (`code`)
return ( 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} {children}
</code> </code>
); );
@ -320,9 +328,7 @@ const MarkdownRenderer = ({
}, },
strong: { strong: {
component: ({ children }) => ( component: ({ children }) => (
<strong className="font-bold text-black dark:text-white"> <strong className="font-bold">{children}</strong>
{children}
</strong>
), ),
}, },
pre: { pre: {
@ -364,11 +370,11 @@ const MarkdownRenderer = ({
<div className="relative"> <div className="relative">
<Markdown <Markdown
className={cn( 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-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-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', 'prose-strong:font-bold',
'break-words text-black dark:text-white max-w-full', 'break-words max-w-full',
className, className,
)} )}
options={markdownOverrides} options={markdownOverrides}

View file

@ -19,7 +19,7 @@ const Copy = ({
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 1000); 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} />} {copied ? <Check size={18} /> : <ClipboardList size={18} />}
</button> </button>

View file

@ -39,7 +39,7 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
<div className="relative"> <div className="relative">
<button <button
ref={buttonRef} 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)} onClick={() => setShowPopover(!showPopover)}
aria-label="Show model information" aria-label="Show model information"
> >
@ -48,25 +48,19 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
{showPopover && ( {showPopover && (
<div <div
ref={popoverRef} 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"> <div className="py-2 px-3">
<h4 className="text-sm font-medium mb-2 text-black dark:text-white"> <h4 className="text-sm font-medium mb-2">Model Information</h4>
Model Information
</h4>
<div className="space-y-1 text-xs"> <div className="space-y-1 text-xs">
<div className="flex space-x-2"> <div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70">Model:</span> <span className="">Model:</span>
<span className="text-black dark:text-white font-medium"> <span className="font-medium">{modelName}</span>
{modelName}
</span>
</div> </div>
{modelStats?.responseTime && ( {modelStats?.responseTime && (
<div className="flex space-x-2"> <div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70"> <span>Response time:</span>
Response time: <span className="font-medium">
</span>
<span className="text-black dark:text-white font-medium">
{(modelStats.responseTime / 1000).toFixed(2)}s {(modelStats.responseTime / 1000).toFixed(2)}s
</span> </span>
</div> </div>
@ -74,26 +68,20 @@ const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
{modelStats?.usage && ( {modelStats?.usage && (
<> <>
<div className="flex space-x-2"> <div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70"> <span>Input tokens:</span>
Input tokens: <span className="font-medium">
</span>
<span className="text-black dark:text-white font-medium">
{modelStats.usage.input_tokens.toLocaleString()} {modelStats.usage.input_tokens.toLocaleString()}
</span> </span>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70"> <span>Output tokens:</span>
Output tokens: <span className="font-medium">
</span>
<span className="text-black dark:text-white font-medium">
{modelStats.usage.output_tokens.toLocaleString()} {modelStats.usage.output_tokens.toLocaleString()}
</span> </span>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<span className="text-black/70 dark:text-white/70"> <span>Total tokens:</span>
Total tokens: <span className="font-medium">
</span>
<span className="text-black dark:text-white font-medium">
{modelStats.usage.total_tokens.toLocaleString()} {modelStats.usage.total_tokens.toLocaleString()}
</span> </span>
</div> </div>

View file

@ -10,7 +10,7 @@ const Rewrite = ({
return ( return (
<button <button
onClick={() => rewrite(messageId)} 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} /> <ArrowLeftRight size={18} />
<p className="text-xs font-medium">Rewrite</p> <p className="text-xs font-medium">Rewrite</p>

View file

@ -70,7 +70,7 @@ const MessageBox = ({
{isEditing ? ( {isEditing ? (
<div className="w-full"> <div className="w-full">
<textarea <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} value={editedContent}
onChange={(e) => setEditedContent(e.target.value)} onChange={(e) => setEditedContent(e.target.value)}
placeholder="Edit your message..." placeholder="Edit your message..."
@ -79,7 +79,7 @@ const MessageBox = ({
<div className="flex flex-row space-x-2 mt-3 justify-end"> <div className="flex flex-row space-x-2 mt-3 justify-end">
<button <button
onClick={cancelEditMessage} 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" aria-label="Cancel"
title="Cancel" title="Cancel"
> >
@ -87,27 +87,24 @@ const MessageBox = ({
</button> </button>
<button <button
onClick={saveEditMessage} 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" aria-label="Save changes"
title="Save changes" title="Save changes"
disabled={!editedContent.trim()} disabled={!editedContent.trim()}
> >
<Check size={18} className="text-white" /> <Check size={18} />
</button> </button>
</div> </div>
</div> </div>
) : ( ) : (
<> <>
<div className="flex items-center"> <div className="flex items-center">
<h2 <h2 className="font-medium text-3xl" onClick={startEditMessage}>
className="text-black dark:text-white font-medium text-3xl"
onClick={startEditMessage}
>
{message.content} {message.content}
</h2> </h2>
<button <button
onClick={startEditMessage} 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" aria-label="Edit message"
title="Edit message" title="Edit message"
> >

View file

@ -11,22 +11,17 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
return ( return (
<div className="flex flex-col space-y-4 w-full lg:w-9/12"> <div className="flex flex-col space-y-4 w-full lg:w-9/12">
{progress && progress.current !== progress.total ? ( {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"> <div className="flex flex-col space-y-3">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold">{progress.message}</p>
{progress.message}
</p>
{progress.subMessage && ( {progress.subMessage && (
<p <p className="text-xs mt-1" title={progress.subMessage}>
className="text-xs text-black/40 dark:text-white/40 mt-1"
title={progress.subMessage}
>
{progress.subMessage} {progress.subMessage}
</p> </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 <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' progress.current === progress.total ? '' : 'animate-pulse'
}`} }`}
style={{ style={{
@ -39,9 +34,9 @@ const MessageBoxLoading = ({ progress }: MessageBoxLoadingProps) => {
) : ( ) : (
<div className="pl-3 flex items-center justify-start"> <div className="pl-3 flex items-center justify-start">
<div className="flex space-x-1"> <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-fg/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-fg/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]"></div>
</div> </div>
</div> </div>
)} )}

View file

@ -141,14 +141,14 @@ const MessageInput = ({
}} }}
className="w-full" 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-col bg-surface px-3 pt-4 pb-2 rounded-lg w-full border border-surface-2">
<div className="flex flex-row items-end space-x-2 mb-2"> <div className="flex flex-row space-x-2 mb-2">
<TextareaAutosize <TextareaAutosize
ref={inputRef} ref={inputRef}
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
minRows={1} 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'} placeholder={firstMessage ? 'Ask anything...' : 'Ask a follow-up'}
autoFocus={true} autoFocus={true}
/> />
@ -196,7 +196,7 @@ const MessageInput = ({
aria-label="Cancel" aria-label="Cancel"
> >
{loading && ( {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]"> <span className="relative flex items-center justify-center w-[17px] h-[17px]">
<Square size={17} className="text-white" /> <Square size={17} className="text-white" />
@ -205,13 +205,13 @@ const MessageInput = ({
) : ( ) : (
<button <button
disabled={message.trim().length === 0} 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" type="submit"
> >
{firstMessage ? ( {firstMessage ? (
<ArrowRight className="bg-background" size={17} /> <ArrowRight size={17} />
) : ( ) : (
<ArrowUp className="bg-background" size={17} /> <ArrowUp size={17} />
)} )}
</button> </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', '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' : '', files.length > 0 ? '-ml-2 lg:-ml-3' : '',
isDisabled isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed' ? 'text-fg/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/50 hover:bg-surface-2 hover:text-fg',
)} )}
> >
{files.length > 1 && ( {files.length > 1 && (
<> <>
<File <File
size={19} size={19}
className={isDisabled ? 'text-sky-900' : 'text-sky-400'} className={isDisabled ? 'text-fg/20' : 'text-accent'}
/> />
<p <p
className={cn( className={cn(
'inline whitespace-nowrap text-xs font-medium', 'inline whitespace-nowrap text-xs font-medium',
isDisabled ? 'text-sky-900' : 'text-sky-400', isDisabled ? 'text-fg/20' : 'text-accent',
)} )}
> >
{files.length} files {files.length} files
@ -109,12 +109,12 @@ const Attach = ({
<> <>
<File <File
size={18} size={18}
className={isDisabled ? 'text-sky-900' : 'text-sky-400'} className={isDisabled ? 'text-fg/20' : 'text-accent'}
/> />
<p <p
className={cn( className={cn(
'text-xs font-medium', 'text-xs font-medium',
isDisabled ? 'text-sky-900' : 'text-sky-400', isDisabled ? 'text-fg/20' : 'text-accent',
)} )}
> >
{files[0].fileName.length > 10 {files[0].fileName.length > 10
@ -136,11 +136,9 @@ const Attach = ({
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<PopoverPanel className="absolute z-10 w-64 md:w-[350px] right-0"> <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"> <div className="flex flex-row items-center justify-between px-3 py-2">
<h4 className="text-black dark:text-white font-medium text-sm"> <h4 className="text-fg font-medium text-sm">Attached files</h4>
Attached files
</h4>
<div className="flex flex-row items-center space-x-4"> <div className="flex flex-row items-center space-x-4">
<button <button
type="button" type="button"
@ -149,8 +147,8 @@ const Attach = ({
className={cn( className={cn(
'flex flex-row items-center space-x-1 transition duration-200', 'flex flex-row items-center space-x-1 transition duration-200',
isDisabled isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed' ? 'text-fg/20 cursor-not-allowed'
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white', : 'text-fg/70 hover:text-fg',
)} )}
> >
<input <input
@ -176,8 +174,8 @@ const Attach = ({
className={cn( className={cn(
'flex flex-row items-center space-x-1 transition duration-200', 'flex flex-row items-center space-x-1 transition duration-200',
isDisabled isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed' ? 'text-fg/20 cursor-not-allowed'
: 'text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white', : 'text-fg/70 hover:text-fg',
)} )}
> >
<Trash size={14} /> <Trash size={14} />
@ -185,17 +183,17 @@ const Attach = ({
</button> </button>
</div> </div>
</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"> <div className="flex flex-col items-center">
{files.map((file, i) => ( {files.map((file, i) => (
<div <div
key={i} key={i}
className="flex flex-row items-center justify-start w-full space-x-3 p-3" 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"> <div className="bg-surface-2 flex items-center justify-center w-10 h-10 rounded-md">
<File size={16} className="text-white/70" /> <File size={16} className="text-fg/70" />
</div> </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.length > 25
? file.fileName.replace(/\.\w+$/, '').substring(0, 25) + ? file.fileName.replace(/\.\w+$/, '').substring(0, 25) +
'...' + '...' +
@ -211,9 +209,9 @@ const Attach = ({
</Popover> </Popover>
{isSpeedMode && ( {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="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 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>
</div> </div>
)} )}
@ -227,8 +225,8 @@ const Attach = ({
className={cn( className={cn(
'flex flex-row items-center space-x-1 rounded-xl transition duration-200 p-2', 'flex flex-row items-center space-x-1 rounded-xl transition duration-200 p-2',
isDisabled isDisabled
? 'text-black/20 dark:text-white/20 cursor-not-allowed' ? 'text-fg/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/50 hover:bg-surface-2 hover:text-fg',
)} )}
> >
<input <input
@ -244,9 +242,9 @@ const Attach = ({
</button> </button>
{isSpeedMode && ( {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="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 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>
</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', key: 'webSearch',
title: 'All', title: 'All',
description: 'Searches across all of the internet', description: 'Searches across all of the internet',
icon: <Globe size={20} className="text-[#24A0ED]" />, icon: <Globe size={20} className="text-accent" />,
}, },
{ {
key: 'chat', key: 'chat',
@ -42,17 +42,17 @@ const Focus = ({
); );
return ( 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="flex flex-row items-center space-x-1">
<div className="relative"> <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 */} {/* Web Search Mode Icon */}
<button <button
className={cn( className={cn(
'p-2 transition-all duration-200', 'p-2 transition-all duration-200',
focusMode === 'webSearch' focusMode === 'webSearch'
? 'bg-[#24A0ED]/20 text-[#24A0ED] scale-105' ? 'text-accent 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/70',
)} )}
onMouseEnter={() => setShowWebSearchTooltip(true)} onMouseEnter={() => setShowWebSearchTooltip(true)}
onMouseLeave={() => setShowWebSearchTooltip(false)} onMouseLeave={() => setShowWebSearchTooltip(false)}
@ -65,15 +65,15 @@ const Focus = ({
</button> </button>
{/* Divider */} {/* 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 */} {/* Chat Mode Icon */}
<button <button
className={cn( className={cn(
'p-2 transition-all duration-200', 'p-2 transition-all duration-200',
focusMode === 'chat' focusMode === 'chat'
? 'bg-[#10B981]/20 text-[#10B981] scale-105' ? '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-fg/70',
)} )}
onMouseEnter={() => setShowChatTooltip(true)} onMouseEnter={() => setShowChatTooltip(true)}
onMouseLeave={() => setShowChatTooltip(false)} onMouseLeave={() => setShowChatTooltip(false)}
@ -86,15 +86,15 @@ const Focus = ({
</button> </button>
{/* Divider */} {/* 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 */} {/* Local Research Mode Icon */}
<button <button
className={cn( className={cn(
'p-2 transition-all duration-200', 'p-2 transition-all duration-200',
focusMode === 'localResearch' focusMode === 'localResearch'
? 'bg-[#8B5CF6]/20 text-[#8B5CF6] scale-105' ? '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-fg/70',
)} )}
onMouseEnter={() => setShowLocalResearchTooltip(true)} onMouseEnter={() => setShowLocalResearchTooltip(true)}
onMouseLeave={() => setShowLocalResearchTooltip(false)} onMouseLeave={() => setShowLocalResearchTooltip(false)}
@ -110,14 +110,14 @@ const Focus = ({
{/* Web Search Mode Tooltip */} {/* Web Search Mode Tooltip */}
{showWebSearchTooltip && ( {showWebSearchTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 animate-in fade-in-0 duration-150"> <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"> <div className="flex items-center space-x-2 mb-2">
<Globe size={16} className="text-[#24A0ED]" /> <Globe size={16} className="text-accent" />
<h3 className="font-medium text-sm text-black dark:text-white text-left"> <h3 className="font-medium text-sm text-left">
{webSearchMode?.title} {webSearchMode?.title}
</h3> </h3>
</div> </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} {webSearchMode?.description}
</p> </p>
</div> </div>
@ -127,14 +127,14 @@ const Focus = ({
{/* Chat Mode Tooltip */} {/* Chat Mode Tooltip */}
{showChatTooltip && ( {showChatTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 transform animate-in fade-in-0 duration-150"> <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"> <div className="flex items-center space-x-2 mb-2">
<MessageCircle size={16} className="text-[#10B981]" /> <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} {chatMode?.title}
</h3> </h3>
</div> </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} {chatMode?.description}
</p> </p>
</div> </div>
@ -144,14 +144,14 @@ const Focus = ({
{/* Local Research Mode Tooltip */} {/* Local Research Mode Tooltip */}
{showLocalResearchTooltip && ( {showLocalResearchTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 left-0 animate-in fade-in-0 duration-150"> <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"> <div className="flex items-center space-x-2 mb-2">
<Pencil size={16} className="text-[#8B5CF6]" /> <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} {localResearchMode?.title}
</h3> </h3>
</div> </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} {localResearchMode?.description}
</p> </p>
</div> </div>

View file

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

View file

@ -47,18 +47,18 @@ const Optimization = ({
<button <button
type="button" type="button"
onClick={handleToggle} 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="flex flex-row items-center space-x-1">
<div className="relative"> <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 */} {/* Speed Mode Icon */}
<div <div
className={cn( className={cn(
'p-2 transition-all duration-200', 'p-2 transition-all duration-200',
!isAgentMode !isAgentMode
? 'bg-[#FF9800]/20 text-[#FF9800] scale-105' ? '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)} onMouseEnter={() => setShowSpeedTooltip(true)}
onMouseLeave={() => setShowSpeedTooltip(false)} onMouseLeave={() => setShowSpeedTooltip(false)}
@ -67,7 +67,7 @@ const Optimization = ({
</div> </div>
{/* Divider */} {/* 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 */} {/* Agent Mode Icon */}
<div <div
@ -75,7 +75,7 @@ const Optimization = ({
'p-2 transition-all duration-200', 'p-2 transition-all duration-200',
isAgentMode isAgentMode
? 'bg-[#9C27B0]/20 text-[#9C27B0] scale-105' ? '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)} onMouseEnter={() => setShowAgentTooltip(true)}
onMouseLeave={() => setShowAgentTooltip(false)} onMouseLeave={() => setShowAgentTooltip(false)}
@ -87,14 +87,14 @@ const Optimization = ({
{/* Speed Mode Tooltip */} {/* Speed Mode Tooltip */}
{showSpeedTooltip && ( {showSpeedTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 right-0 animate-in fade-in-0 duration-150"> <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"> <div className="flex items-center space-x-2 mb-2">
<Zap size={16} className="text-[#FF9800]" /> <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} {speedMode?.title}
</h3> </h3>
</div> </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} {speedMode?.description}
</p> </p>
</div> </div>
@ -104,14 +104,14 @@ const Optimization = ({
{/* Agent Mode Tooltip */} {/* Agent Mode Tooltip */}
{showAgentTooltip && ( {showAgentTooltip && (
<div className="absolute z-20 bottom-[100%] mb-2 right-0 animate-in fade-in-0 duration-150"> <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"> <div className="flex items-center space-x-2 mb-2">
<Bot size={16} className="text-[#9C27B0]" /> <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} {agentMode?.title}
</h3> </h3>
</div> </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} {agentMode?.description}
</p> </p>
</div> </div>

View file

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

View file

@ -80,10 +80,10 @@ const ToolSelector = ({
<> <>
<PopoverButton <PopoverButton
className={cn( 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 selectedCount > 0
? 'text-[#24A0ED] hover:text-blue-200' ? 'text-accent hover:text-accent'
: 'text-black/60 hover:text-black/30 dark:text-white/60 dark:hover:*:text-white/30', : 'text-fg/60 hover:text-fg/30',
)} )}
title="Select Tools" title="Select Tools"
> >
@ -101,23 +101,23 @@ const ToolSelector = ({
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<PopoverPanel className="absolute z-20 w-72 transform bottom-full mb-2"> <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="overflow-hidden rounded-lg shadow-lg bg-surface border border-surface-2">
<div className="px-4 py-3 border-b border-light-200 dark:border-dark-200"> <div className="px-4 py-3 border-b border-surface-2">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90"> <h3 className="text-sm font-medium text-fg/90">
Select Tools Select Tools
</h3> </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. Choose tools to assist the AI.
</p> </p>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="px-4 py-3"> <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>
) : ( ) : (
<div className="max-h-60 overflow-y-auto p-1.5 space-y-0.5"> <div className="max-h-60 overflow-y-auto p-1.5 space-y-0.5">
{availableTools.length === 0 && ( {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. No tools available.
</p> </p>
)} )}
@ -126,27 +126,27 @@ const ToolSelector = ({
<div <div
key={tool.name} key={tool.name}
onClick={() => handleToggleTool(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) ? ( {selectedToolNames.includes(tool.name) ? (
<CheckSquare <CheckSquare
size={18} size={18}
className="text-[#24A0ED] flex-shrink-0 mt-0.5" className="text-accent flex-shrink-0 mt-0.5"
/> />
) : ( ) : (
<Square <Square
size={18} 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"> <div className="flex-1 min-w-0">
<span <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} title={tool.name}
> >
{tool.name.replace(/_/g, ' ')} {tool.name.replace(/_/g, ' ')}
</span> </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} {tool.description}
</p> </p>
</div> </div>

View file

@ -17,7 +17,7 @@ const MessageSource = ({
}: MessageSourceProps) => { }: MessageSourceProps) => {
return ( return (
<a <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} href={source.metadata.url}
target="_blank" target="_blank"
style={style} style={style}
@ -25,8 +25,8 @@ const MessageSource = ({
{/* Left side: Favicon/Icon and source number */} {/* Left side: Favicon/Icon and source number */}
<div className="flex flex-col items-center space-y-2 flex-shrink-0"> <div className="flex flex-col items-center space-y-2 flex-shrink-0">
{source.metadata.url === 'File' ? ( {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"> <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-white/70" /> <File size={16} className="text-fg/70" />
</div> </div>
) : ( ) : (
<img <img
@ -37,38 +37,29 @@ const MessageSource = ({
className="rounded-lg h-7 w-7" 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' && ( {typeof index === 'number' && (
<span className="font-semibold">{index + 1}</span> <span className="font-semibold">{index + 1}</span>
)} )}
{/* Processing type indicator */} {/* Processing type indicator */}
{source.metadata.processingType === 'preview-only' && ( {source.metadata.processingType === 'preview-only' && (
<span title="Partial content analyzed" className="inline-flex"> <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> </span>
)} )}
{source.metadata.processingType === 'full-content' && ( {source.metadata.processingType === 'full-content' && (
<span title="Full content analyzed" className="inline-flex"> <span title="Full content analyzed" className="inline-flex">
<Microscope <Microscope size={12} className="text-fg/40" />
size={12}
className="text-black/40 dark:text-white/40"
/>
</span> </span>
)} )}
{source.metadata.processingType === 'url-direct-content' && ( {source.metadata.processingType === 'url-direct-content' && (
<span title="Direct URL content" className="inline-flex"> <span title="Direct URL content" className="inline-flex">
<FileText <FileText size={12} className="text-fg/40" />
size={12}
className="text-black/40 dark:text-white/40"
/>
</span> </span>
)} )}
{source.metadata.processingType === 'url-content-extraction' && ( {source.metadata.processingType === 'url-content-extraction' && (
<span title="Summarized URL content" className="inline-flex"> <span title="Summarized URL content" className="inline-flex">
<Sparkles <Sparkles size={12} className="text-fg/40" />
size={12}
className="text-black/40 dark:text-white/40"
/>
</span> </span>
)} )}
</div> </div>
@ -77,18 +68,18 @@ const MessageSource = ({
{/* Right side: Content */} {/* Right side: Content */}
<div className="flex-1 flex flex-col space-y-2"> <div className="flex-1 flex flex-col space-y-2">
{/* Title */} {/* 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} {source.metadata.title}
</h3> </h3>
{/* URL */} {/* 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, '')} {source.metadata.url.replace(/.+\/\/|www.|\..+/g, '')}
</p> </p>
{/* Preview content */} {/* Preview content */}
<p <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={{ style={{
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 3, WebkitLineClamp: 3,

View file

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

View file

@ -159,7 +159,7 @@ const Navbar = ({
}, []); }, []);
return ( 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 <a
href="/" href="/"
className="active:scale-95 transition duration-100 cursor-pointer lg:hidden" 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"> <div className="flex flex-row items-center space-x-4">
<Popover className="relative"> <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} /> <Share size={17} />
</PopoverButton> </PopoverButton>
<Transition <Transition
@ -186,20 +186,20 @@ const Navbar = ({
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" 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"> <div className="flex flex-col py-3 px-3 gap-2">
<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={() => exportAsMarkdown(messages, title || '')} onClick={() => exportAsMarkdown(messages, title || '')}
> >
<FileText size={17} className="text-[#24A0ED]" /> <FileText size={17} className="text-accent" />
Export as Markdown Export as Markdown
</button> </button>
<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 || '')} onClick={() => exportAsPDF(messages, title || '')}
> >
<FileDown size={17} className="text-[#24A0ED]" /> <FileDown size={17} className="text-accent" />
Export as PDF Export as PDF
</button> </button>
</div> </div>

View file

@ -27,14 +27,14 @@ const NewsArticleWidget = () => {
}, []); }, []);
return ( 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 ? ( {loading ? (
<> <>
<div className="animate-pulse flex flex-row items-center w-full h-full"> <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="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-4 w-3/4 rounded bg-surface-2" />
<div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" /> <div className="h-3 w-1/2 rounded bg-surface-2" />
</div> </div>
</div> </div>
</> </>
@ -46,7 +46,7 @@ const NewsArticleWidget = () => {
className="flex flex-row items-center w-full h-full group" className="flex flex-row items-center w-full h-full group"
> >
<img <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={ src={
new URL(article.thumbnail).origin + new URL(article.thumbnail).origin +
new URL(article.thumbnail).pathname + new URL(article.thumbnail).pathname +
@ -55,10 +55,10 @@ const NewsArticleWidget = () => {
alt={article.title} alt={article.title}
/> />
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0"> <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} {article.title}
</div> </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} {article.content}
</p> </p>
</div> </div>

View file

@ -126,7 +126,7 @@ const SearchImages = ({
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div <div
key={i} 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> </div>
@ -158,7 +158,7 @@ const SearchImages = ({
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
<button <button
onClick={handleShowMore} 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>Show More Images</span>
<span className="text-sm opacity-75"> <span className="text-sm opacity-75">

View file

@ -144,7 +144,7 @@ const Searchvideos = ({
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div <div
key={i} 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> </div>
@ -173,7 +173,7 @@ const Searchvideos = ({
alt={video.title} alt={video.title}
className="relative h-full w-full aspect-video object-cover rounded-lg" 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} /> <PlayCircle size={15} />
<p className="text-xs">Video</p> <p className="text-xs">Video</p>
</div> </div>
@ -184,7 +184,7 @@ const Searchvideos = ({
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
<button <button
onClick={handleShowMore} 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>Show More Videos</span>
<span className="text-sm opacity-75"> <span className="text-sm opacity-75">

View file

@ -53,7 +53,7 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div> <div>
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-20 lg:flex-col"> <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="/"> <a href="/">
<SquarePen className="cursor-pointer" /> <SquarePen className="cursor-pointer" />
</a> </a>
@ -63,15 +63,13 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
key={i} key={i}
href={link.href} href={link.href}
className={cn( 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', '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 link.active ? 'text-fg' : 'text-fg/70',
? 'text-black dark:text-white'
: 'text-black/70 dark:text-white/70',
)} )}
> >
<link.icon /> <link.icon />
{link.active && ( {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> </Link>
))} ))}
@ -83,20 +81,18 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
</div> </div>
</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) => ( {navLinks.map((link, i) => (
<Link <Link
href={link.href} href={link.href}
key={i} key={i}
className={cn( className={cn(
'relative flex flex-col items-center space-y-1 text-center w-full', 'relative flex flex-col items-center space-y-1 text-center w-full',
link.active link.active ? 'text-fg' : 'text-fg/70',
? 'text-black dark:text-white'
: 'text-black dark:text-white/70',
)} )}
> >
{link.active && ( {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 /> <link.icon />
<p className="text-xs">{link.label}</p> <p className="text-xs">{link.label}</p>

View file

@ -24,27 +24,24 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
onToggle || (() => setInternalExpanded(!internalExpanded)); onToggle || (() => setInternalExpanded(!internalExpanded));
return ( 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 <button
onClick={handleToggle} 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"> <div className="flex items-center space-x-2">
<BrainCircuit <BrainCircuit size={20} className="text-[#9C27B0]" />
size={20}
className="text-[#9C27B0] dark:text-[#CE93D8]"
/>
<span className="font-medium text-sm">Thinking Process</span> <span className="font-medium text-sm">Thinking Process</span>
</div> </div>
{isExpanded ? ( {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> </button>
{isExpanded && ( {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} {content}
</div> </div>
)} )}

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import {
ChevronUp, ChevronUp,
GripVertical, GripVertical,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import MarkdownRenderer from '@/components/MarkdownRenderer'; import MarkdownRenderer from '@/components/MarkdownRenderer';
import { Widget } from '@/lib/types/widget'; 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"> <div className="flex items-center space-x-2 flex-1 min-w-0">
{/* Drag Handle */} {/* Drag Handle */}
<div <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" title="Drag to move widget"
> >
<GripVertical <GripVertical size={16} className="text-fg/50" />
size={16}
className="text-gray-400 dark:text-gray-500"
/>
</div> </div>
<CardTitle className="text-lg font-medium truncate"> <CardTitle className="text-lg font-medium truncate">
@ -73,7 +71,7 @@ const WidgetDisplay = ({
<div className="flex items-center space-x-2 flex-shrink-0"> <div className="flex items-center space-x-2 flex-shrink-0">
{/* Last updated date with refresh frequency tooltip */} {/* Last updated date with refresh frequency tooltip */}
<span <span
className="text-xs text-gray-500 dark:text-gray-400" className="text-xs text-fg/60"
title={getRefreshFrequencyText()} title={getRefreshFrequencyText()}
> >
{formatLastUpdated(widget.lastUpdated)} {formatLastUpdated(widget.lastUpdated)}
@ -83,12 +81,15 @@ const WidgetDisplay = ({
<button <button
onClick={() => onRefresh(widget.id)} onClick={() => onRefresh(widget.id)}
disabled={widget.isLoading} 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" title="Refresh Widget"
> >
<RefreshCw <RefreshCw
size={16} size={16}
className={`text-gray-600 dark:text-gray-400 ${widget.isLoading ? 'animate-spin' : ''}`} className={cn(
'text-fg/70',
widget.isLoading ? 'animate-spin' : '',
)}
/> />
</button> </button>
</div> </div>
@ -98,31 +99,29 @@ const WidgetDisplay = ({
<CardContent className="flex-1 overflow-hidden"> <CardContent className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
{widget.isLoading ? ( {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" /> <RefreshCw size={20} className="animate-spin mr-2" />
<span>Loading content...</span> <span>Loading content...</span>
</div> </div>
) : widget.error ? ( ) : 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 <AlertCircle
size={16} size={16}
className="text-red-500 mt-0.5 flex-shrink-0" className="text-red-500 mt-0.5 flex-shrink-0"
/> />
<div className="flex-1"> <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 Error Loading Content
</p> </p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1"> <p className="text-xs text-red-600 mt-1">{widget.error}</p>
{widget.error}
</p>
</div> </div>
</div> </div>
) : widget.content ? ( ) : 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} /> <MarkdownRenderer content={widget.content} showThinking={false} />
</div> </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"> <div className="text-center">
<p className="text-sm">No content yet</p> <p className="text-sm">No content yet</p>
<p className="text-xs mt-1">Click refresh to load content</p> <p className="text-xs mt-1">Click refresh to load content</p>
@ -133,10 +132,10 @@ const WidgetDisplay = ({
</CardContent> </CardContent>
{/* Collapsible footer with sources and actions */} {/* 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 <button
onClick={() => setIsFooterExpanded(!isFooterExpanded)} 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 ? ( {isFooterExpanded ? (
<ChevronUp size={14} /> <ChevronUp size={14} />
@ -151,22 +150,16 @@ const WidgetDisplay = ({
{/* Sources */} {/* Sources */}
{widget.sources.length > 0 && ( {widget.sources.length > 0 && (
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2"> <p className="text-xs text-fg/60 mb-2">Sources:</p>
Sources:
</p>
<div className="space-y-1"> <div className="space-y-1">
{widget.sources.map((source, index) => ( {widget.sources.map((source, index) => (
<div <div
key={index} key={index}
className="flex items-center space-x-2 text-xs" 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="inline-block w-2 h-2 bg-accent rounded-full"></span>
<span className="text-gray-600 dark:text-gray-300 truncate"> <span className="text-fg/70 truncate">{source.url}</span>
{source.url} <span className="text-fg/60">({source.type})</span>
</span>
<span className="text-gray-400 dark:text-gray-500">
({source.type})
</span>
</div> </div>
))} ))}
</div> </div>
@ -177,7 +170,7 @@ const WidgetDisplay = ({
<div className="flex items-center space-x-2 pt-2"> <div className="flex items-center space-x-2 pt-2">
<button <button
onClick={() => onEdit(widget)} 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} /> <Edit size={12} />
<span>Edit</span> <span>Edit</span>
@ -185,7 +178,7 @@ const WidgetDisplay = ({
<button <button
onClick={() => onDelete(widget.id)} 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} /> <Trash2 size={12} />
<span>Delete</span> <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'; 'use client';
import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import Select from '../ui/Select';
type Theme = 'dark' | 'light' | 'system'; type Theme = 'dark' | 'light' | 'custom';
const ThemeSwitcher = ({ className }: { className?: string }) => { const ThemeSwitcher = ({ className }: { className?: string }) => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState<Theme>('dark');
const { theme, setTheme } = useTheme(); const [bg, setBg] = useState<string>('');
const [accent, setAccent] = useState<string>('');
const isTheme = useCallback((t: Theme) => t === theme, [theme]);
const handleThemeSwitch = (theme: Theme) => {
setTheme(theme);
};
useEffect(() => { useEffect(() => {
setMounted(true); 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(() => { const apply = (next: Theme, nextBg = bg, nextAccent = accent) => {
if (isTheme('system')) { (window as any).__setAppTheme?.(next, nextBg, nextAccent);
const preferDarkScheme = window.matchMedia( setTheme(next);
'(prefers-color-scheme: dark)', if (next === 'light' || next === 'dark') {
); // Refresh local color inputs from storage so UI shows current defaults
const b = localStorage.getItem('userBg') || '#0f0f0f';
const detectThemeChange = (event: MediaQueryListEvent) => { const a = localStorage.getItem('userAccent') || '#2563eb';
const theme: Theme = event.matches ? 'dark' : 'light'; setBg(b);
setTheme(theme); setAccent(a);
}
}; };
preferDarkScheme.addEventListener('change', detectThemeChange); if (!mounted) return null;
return () => {
preferDarkScheme.removeEventListener('change', detectThemeChange);
};
}
}, [isTheme, setTheme, theme]);
// Avoid Hydration Mismatch
if (!mounted) {
return null;
}
return ( return (
<Select <div className={className}>
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} value={theme}
onChange={(e) => handleThemeSwitch(e.target.value as Theme)} onChange={(e) => apply(e.target.value as Theme)}
options={[ >
{ value: 'light', label: 'Light' }, <option value="light">Light</option>
{ value: 'dark', label: 'Dark' }, <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 <select
{...restProps} {...restProps}
className={cn( 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, 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 - 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 - 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 - 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 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 - You have access to ${fileIds.length} uploaded file${fileIds.length === 1 ? '' : 's'} that may contain relevant information

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'; // Tailwind v4 uses CSS-first configuration via @theme in globals.css.
import type { DefaultColors } from 'tailwindcss/types/generated/colors'; // Keeping an empty config to satisfy tooling that expects this file.
export default {};
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;

745
yarn.lock

File diff suppressed because it is too large Load diff