feat(themes): Added custom theme support.
This commit is contained in:
parent
58a3f8efbc
commit
2222928623
48 changed files with 2273 additions and 1590 deletions
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
|
|
@ -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
1300
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 "Run Preview" to see how your widget
|
Click "Run Preview" 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'}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
209
src/components/theme/Controller.tsx
Normal file
209
src/components/theme/Controller.tsx
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -332,7 +333,7 @@ Your task is to provide answers that are:
|
||||||
- Focus your file searches on specific aspects of the user's query that might be covered in the uploaded documents
|
- Focus your file searches on specific aspects of the user's query that might be covered in the uploaded documents
|
||||||
- **Important**: You do NOT need to specify file IDs - the tool will automatically search through all available uploaded files.`
|
- **Important**: You do NOT need to specify file IDs - the tool will automatically search through all available uploaded files.`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
3. **Supplement**: (\`url_summarization\` tool) Retrieve specific sources if necessary to extract key points not covered in the initial search or disambiguate findings
|
3. **Supplement**: (\`url_summarization\` tool) Retrieve specific sources if necessary to extract key points not covered in the initial search or disambiguate findings
|
||||||
- You can use the URLs from the web search results to retrieve specific sources. They must be passed to the tool unchanged
|
- You can use the URLs from the web search results to retrieve specific sources. They must be passed to the tool unchanged
|
||||||
- URLs can be passed as an array to request multiple sources at once
|
- URLs can be passed as an array to request multiple sources at once
|
||||||
|
|
|
||||||
213
src/lib/utils/color-utils.ts
Normal file
213
src/lib/utils/color-utils.ts
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue