fix: side bar height
This commit is contained in:
@@ -141,5 +141,4 @@ git tag -d v1.0.0; git push origin :refs/tags/v1.0.0; git tag -a v1.0.0 -m "Rele
|
|||||||
|
|
||||||
## To implement
|
## To implement
|
||||||
|
|
||||||
- Better Tailwind CSS classes
|
- Token refresh
|
||||||
- See keycloak user infos
|
|
||||||
|
|||||||
+201
-26
@@ -31,38 +31,213 @@
|
|||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
'use client'
|
// 'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
// import { useEffect, useState } from 'react';
|
||||||
import Spinner from '@/components/spinner'
|
// import Spinner from '@/components/spinner';
|
||||||
|
|
||||||
export default function AboutPage() {
|
// export default function AboutPage() {
|
||||||
const [data, setData] = useState<any>(null)
|
// const [data, setData] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true)
|
// const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
fetch((process.env.NEXT_PUBLIC_API_URL as string) + '/docs.json')
|
// fetch((process.env.NEXT_PUBLIC_API_URL as string) + '/docs.json')
|
||||||
.then((res) => res.json())
|
// .then((res) => res.json())
|
||||||
.then((result) => {
|
// .then((result) => {
|
||||||
setData(result)
|
// setData(result);
|
||||||
setLoading(false)
|
// setLoading(false);
|
||||||
})
|
// })
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
setLoading(false)
|
// setLoading(false);
|
||||||
})
|
// });
|
||||||
}, [])
|
// }, []);
|
||||||
|
|
||||||
if (loading) {
|
// if (loading) {
|
||||||
return (
|
// return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
// <div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="h-12 w-12 rounded-full border-4 border-gray-300 border-t-blue-600 animate-spin" />
|
// <div className="h-12 w-12 rounded-full border-4 border-gray-300 border-t-blue-600 animate-spin" />
|
||||||
</div>
|
// </div>
|
||||||
)
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="flex items-center justify-center min-h-screen bg-primary text-primary transition-theme">
|
||||||
|
// <Spinner />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Sidebar from '@/components/Sidebar';
|
||||||
|
import { SidebarPanel } from '@/components/Sidebar';
|
||||||
|
import {
|
||||||
|
Slider,
|
||||||
|
NumberInput,
|
||||||
|
TextInput,
|
||||||
|
Toggle,
|
||||||
|
} from '../../../components/Inputs';
|
||||||
|
|
||||||
|
type PanelProps = {
|
||||||
|
settings: Settings;
|
||||||
|
setSettings: React.Dispatch<React.SetStateAction<Settings>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
temp: number;
|
||||||
|
topP: number;
|
||||||
|
topK: number | undefined;
|
||||||
|
maxTokens: number | undefined;
|
||||||
|
numCtx: number | undefined;
|
||||||
|
stop: string[] | undefined;
|
||||||
|
repeatPenalty: number;
|
||||||
|
seed: number | undefined;
|
||||||
|
stream: boolean;
|
||||||
|
keepAlive: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingsPanel({ settings, setSettings }: PanelProps) {
|
||||||
|
const set =
|
||||||
|
<K extends keyof Settings>(key: K) =>
|
||||||
|
(val: Settings[K]) =>
|
||||||
|
setSettings((prev) => ({ ...prev, [key]: val }));
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'Sampling',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Slider
|
||||||
|
label="Temperature"
|
||||||
|
value={s.temp}
|
||||||
|
setValue={set('temp')}
|
||||||
|
min={0}
|
||||||
|
max={2}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label="Top P"
|
||||||
|
value={s.topP}
|
||||||
|
setValue={set('topP')}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
/>
|
||||||
|
<NumberInput label="Top K" value={s.topK} setValue={set('topK')} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Generation',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<NumberInput
|
||||||
|
label="Max Tokens"
|
||||||
|
value={s.maxTokens}
|
||||||
|
setValue={set('maxTokens')}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Context Size"
|
||||||
|
value={s.numCtx}
|
||||||
|
setValue={set('numCtx')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Stop Sequences"
|
||||||
|
value={s.stop?.join(', ')}
|
||||||
|
setValue={(val) =>
|
||||||
|
set('stop')(val ? val.split(',').map((s) => s.trim()) : undefined)
|
||||||
}
|
}
|
||||||
|
placeholder="Comma separated"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Control',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Slider
|
||||||
|
label="Repeat Penalty"
|
||||||
|
value={s.repeatPenalty}
|
||||||
|
setValue={set('repeatPenalty')}
|
||||||
|
min={0}
|
||||||
|
max={2}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
<NumberInput label="Seed" value={s.seed} setValue={set('seed')} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'System',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Toggle label="Streaming" value={s.stream} setValue={set('stream')} />
|
||||||
|
<TextInput
|
||||||
|
label="Keep Alive"
|
||||||
|
value={s.keepAlive}
|
||||||
|
setValue={set('keepAlive')}
|
||||||
|
placeholder="e.g. 5m"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-primary text-primary transition-theme">
|
<div className="space-y-6 px-1">
|
||||||
<Spinner/>
|
{sections.map((section) => (
|
||||||
|
<div key={section.title} className="space-y-4">
|
||||||
|
<h3 className="text-xs uppercase text-accent tracking-widest">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{section.fields(settings, set)}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SIDEBAR_PANELS = ({
|
||||||
|
settings,
|
||||||
|
setSettings,
|
||||||
|
}: PanelProps): SidebarPanel[] => [
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: 'Settings',
|
||||||
|
icon: (
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.84 1.804A1 1 0 018.82 1h2.36a1 1 0 01.98.804l.331 1.652a6.993 6.993 0 011.929 1.115l1.598-.54a1 1 0 011.186.447l1.18 2.044a1 1 0 01-.205 1.251l-1.267 1.113a7.047 7.047 0 010 2.228l1.267 1.113a1 1 0 01.205 1.251l-1.18 2.044a1 1 0 01-1.186.447l-1.598-.54a6.993 6.993 0 01-1.929 1.115l-.33 1.652a1 1 0 01-.98.804H8.82a1 1 0 01-.98-.804l-.331-1.652a6.993 6.993 0 01-1.929-1.115l-1.598.54a1 1 0 01-1.186-.447l-1.18-2.044a1 1 0 01.205-1.251l1.267-1.114a7.05 7.05 0 010-2.227L1.821 7.773a1 1 0 01-.206-1.25l1.18-2.045a1 1 0 011.187-.447l1.598.54A6.993 6.993 0 017.51 3.456l.33-1.652zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
component: <SettingsPanel settings={settings} setSettings={setSettings} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function DocPage() {
|
||||||
|
const [settings, setSettings] = useState<Settings>({
|
||||||
|
temp: 0.7,
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 40,
|
||||||
|
maxTokens: 1000,
|
||||||
|
numCtx: 128000,
|
||||||
|
stop: [''],
|
||||||
|
repeatPenalty: 1.1,
|
||||||
|
seed: 0,
|
||||||
|
stream: true,
|
||||||
|
keepAlive: '5m',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 overflow-hidden min-h-0">
|
||||||
|
<Sidebar panels={SIDEBAR_PANELS({ settings, setSettings })} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -40,11 +40,13 @@ export default async function RootLayout(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className={theme === 'dark' ? 'dark' : 'light-mode'}>
|
<html lang={locale} className={theme === 'dark' ? 'dark' : 'light-mode'}>
|
||||||
<body className="min-h-screen flex flex-col bg-primary text-primary transition-theme">
|
<body className="h-screen flex flex-col bg-primary text-primary transition-theme">
|
||||||
<Providers locale={locale} messages={messages} session={session}>
|
<Providers locale={locale} messages={messages} session={session}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<main className="flex-1 pt-[var(--header-height)] flex flex-col">{props.children}</main>
|
<main className="flex-1 pt-[var(--header-height)] flex overflow-hidden">
|
||||||
|
{props.children}
|
||||||
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</Providers>
|
</Providers>
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ export default function Home() {
|
|||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<h1 className="text-5xl font-bold text-cyan-400">{t('name')}</h1>
|
<h1 className="text-5xl font-bold text-cyan-400">{t('name')}</h1>
|
||||||
<p className="mt-4 text-gray-400">{t('welcome')}</p>
|
<p className="mt-4 text-gray-400">{t('welcome')}</p>
|
||||||
<button className="mt-6 px-6 py-3 bg-cyan-500 rounded-xl" onClick={() => signIn('keycloak')}>
|
<button
|
||||||
|
className="mt-6 px-6 py-3 bg-cyan-500 rounded-xl"
|
||||||
|
onClick={() => signIn('keycloak')}
|
||||||
|
>
|
||||||
{t('start')}
|
{t('start')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,11 +40,7 @@ type NumberInputProps = {
|
|||||||
setValue: (val: number | undefined) => void;
|
setValue: (val: number | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NumberInput({
|
export function NumberInput({ label, value, setValue }: NumberInputProps) {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
setValue,
|
|
||||||
}: NumberInputProps) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs uppercase tracking-widest">{label}</label>
|
<label className="text-xs uppercase tracking-widest">{label}</label>
|
||||||
@@ -93,11 +89,7 @@ type ToggleProps = {
|
|||||||
setValue: (val: boolean) => void;
|
setValue: (val: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Toggle({
|
export function Toggle({ label, value, setValue }: ToggleProps) {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
setValue,
|
|
||||||
}: ToggleProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm">{label}</span>
|
<span className="text-sm">{label}</span>
|
||||||
|
|||||||
+132
-56
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useSyncExternalStore } from 'react';
|
import { useState, useSyncExternalStore } from 'react';
|
||||||
|
import { CloseIcon } from './icons/CloseIcon';
|
||||||
|
|
||||||
export type SidebarPanel = {
|
export type SidebarPanel = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,104 +10,179 @@ export type SidebarPanel = {
|
|||||||
component: React.ReactNode;
|
component: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidebarProps = {
|
|
||||||
panels: SidebarPanel[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function useIsWide(): boolean {
|
function useIsWide(): boolean {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
(cb) => {
|
(cb) => {
|
||||||
window.addEventListener('resize', cb);
|
window.addEventListener('resize', cb);
|
||||||
return () => window.removeEventListener('resize', cb);
|
return () => window.removeEventListener('resize', cb);
|
||||||
},
|
},
|
||||||
() => window.innerWidth >= 768, // client snapshot
|
() => window.innerWidth >= 768,
|
||||||
() => false, // server snapshot — must match initial SSR HTML
|
() => false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLOSED = '__closed__';
|
/* -----------------------------
|
||||||
|
Shared sidebar logic
|
||||||
export default function Sidebar({ panels }: SidebarProps) {
|
------------------------------*/
|
||||||
const isWide = useIsWide();
|
function useSidebarState() {
|
||||||
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
|
||||||
const effectiveId =
|
const setClosed = () => setActiveId(null);
|
||||||
activeId === CLOSED ? null
|
|
||||||
: activeId ?? (isWide ? (panels[0]?.id ?? null) : null);
|
|
||||||
|
|
||||||
const activePanel = panels.find((p) => p.id === effectiveId);
|
const toggle = (id: string, effectiveId: string | null) => {
|
||||||
const isOpen = !!activePanel;
|
setActiveId((prev) =>
|
||||||
|
prev === id || (effectiveId === id && prev === null) ? null : id,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const toggle = (id: string) =>
|
const resolveEffectiveId = (isWide: boolean, panels: SidebarPanel[]) => {
|
||||||
setActiveId((prev) => (prev === id || (effectiveId === id && prev === null) ? CLOSED : id));
|
return activeId === null
|
||||||
|
? null
|
||||||
|
: (activeId ?? (isWide ? (panels[0]?.id ?? null) : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeId,
|
||||||
|
setActiveId,
|
||||||
|
setClosed,
|
||||||
|
toggle,
|
||||||
|
resolveEffectiveId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
Desktop UI
|
||||||
|
------------------------------*/
|
||||||
|
function DesktopSidebar({
|
||||||
|
activePanel,
|
||||||
|
isOpen,
|
||||||
|
setClosed,
|
||||||
|
}: {
|
||||||
|
activePanel?: SidebarPanel;
|
||||||
|
isOpen: boolean;
|
||||||
|
setClosed: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex border-r border-t border-muted">
|
|
||||||
{/* Expanded panel */}
|
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
${isWide
|
flex transition-all duration-300 ease-in-out overflow-hidden
|
||||||
? `flex transition-all duration-300 ease-in-out overflow-hidden ${
|
${isOpen ? 'w-64 opacity-100' : 'w-0 opacity-0'}
|
||||||
isOpen ? 'w-64 opacity-100' : 'w-0 opacity-0'
|
|
||||||
}`
|
|
||||||
: isOpen
|
|
||||||
? 'fixed left-14 top-(--var(header-height)) z-30 bg-primary'
|
|
||||||
: 'hidden'
|
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{activePanel && (
|
{activePanel && (
|
||||||
<div
|
<div className="w-64 flex flex-col border-r border-muted">
|
||||||
className={`w-64 flex flex-col border-muted ${
|
<div className="flex items-center justify-between px-4 py-3 border-b border-muted shrink-0">
|
||||||
isWide ? 'border-r' : 'border'
|
<span className="text-sm font-semibold text-accent">
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-muted">
|
|
||||||
<span className="text-sm font-semibold tracking-wide text-accent">
|
|
||||||
{activePanel.label}
|
{activePanel.label}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button onClick={setClosed}>
|
||||||
onClick={() => setActiveId(CLOSED)}
|
<CloseIcon className="w-4 h-4" />
|
||||||
className="text-primary hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
|
|
||||||
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-3">
|
<div className="flex-1 overflow-y-auto p-3 min-h-0">
|
||||||
{activePanel.component}
|
{activePanel.component}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{/* Back drop */}
|
/* -----------------------------
|
||||||
{isOpen && !isWide && (
|
Mobile UI (drawer)
|
||||||
<div
|
------------------------------*/
|
||||||
className="fixed inset-0 bg-black/40 z-20"
|
function MobileSidebar({
|
||||||
onClick={() => setActiveId(CLOSED)}
|
activePanel,
|
||||||
/>
|
setClosed,
|
||||||
)}
|
}: {
|
||||||
|
activePanel?: SidebarPanel;
|
||||||
|
setClosed: () => void;
|
||||||
|
}) {
|
||||||
|
if (!activePanel) return null;
|
||||||
|
|
||||||
{/* Icon rail */}
|
return (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="fixed inset-0 bg-black/40 z-20" onClick={setClosed} />
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<div className="fixed left-14 top-[var(--header-height)] bottom-0 z-30 bg-primary overflow-y-auto border border-muted w-64">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-muted">
|
||||||
|
<span className="text-sm font-semibold text-accent">
|
||||||
|
{activePanel.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button onClick={setClosed}>
|
||||||
|
<CloseIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3">{activePanel.component}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
Icon rail (shared)
|
||||||
|
------------------------------*/
|
||||||
|
function IconRail({
|
||||||
|
panels,
|
||||||
|
effectiveId,
|
||||||
|
toggle,
|
||||||
|
}: {
|
||||||
|
panels: SidebarPanel[];
|
||||||
|
effectiveId: string | null;
|
||||||
|
toggle: (id: string, effectiveId: string | null) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<div className="w-12 flex flex-col items-center py-3 gap-1 z-30">
|
<div className="w-12 flex flex-col items-center py-3 gap-1 z-30">
|
||||||
{panels.map((panel) => (
|
{panels.map((panel) => (
|
||||||
<button
|
<button
|
||||||
key={panel.id}
|
key={panel.id}
|
||||||
onClick={() => toggle(panel.id)}
|
onClick={() => toggle(panel.id, effectiveId)}
|
||||||
title={panel.label}
|
title={panel.label}
|
||||||
className={`w-8 h-8 rounded-lg flex items-center justify-center text-primary transition-theme ${
|
className={`w-8 h-8 rounded-lg flex items-center justify-center transition-theme ${
|
||||||
effectiveId === panel.id
|
effectiveId === panel.id ? 'bg-accent2' : ''
|
||||||
? 'bg-accent2'
|
|
||||||
: ''
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{panel.icon}
|
{panel.icon}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
Main Sidebar (controller)
|
||||||
|
------------------------------*/
|
||||||
|
export default function Sidebar({ panels }: { panels: SidebarPanel[] }) {
|
||||||
|
const isWide = useIsWide();
|
||||||
|
|
||||||
|
const { setClosed, toggle, resolveEffectiveId } = useSidebarState();
|
||||||
|
|
||||||
|
const effectiveId = resolveEffectiveId(isWide, panels);
|
||||||
|
const activePanel = panels.find((p) => p.id === effectiveId);
|
||||||
|
const isOpen = !!activePanel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex border-r border-t border-muted">
|
||||||
|
{/* Desktop */}
|
||||||
|
{isWide && (
|
||||||
|
<DesktopSidebar
|
||||||
|
activePanel={activePanel}
|
||||||
|
isOpen={isOpen}
|
||||||
|
setClosed={setClosed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile */}
|
||||||
|
{!isWide && (
|
||||||
|
<MobileSidebar activePanel={activePanel} setClosed={setClosed} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Icon rail */}
|
||||||
|
<IconRail panels={panels} effectiveId={effectiveId} toggle={toggle} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ export default function ChatUI({
|
|||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [notification, setNotification] = useState<string | null>(null);
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
temp: 0.7,
|
temp: 0.7,
|
||||||
topP: 0.9,
|
topP: 0.9,
|
||||||
@@ -34,7 +35,7 @@ export default function ChatUI({
|
|||||||
stop: [''],
|
stop: [''],
|
||||||
repeatPenalty: 1.1,
|
repeatPenalty: 1.1,
|
||||||
seed: 0,
|
seed: 0,
|
||||||
stream: true,
|
stream: false,
|
||||||
keepAlive: '5m',
|
keepAlive: '5m',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,8 +77,13 @@ export default function ChatUI({
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const assistant = data?.choices?.[0]?.message;
|
const assistant = data?.choices?.[0]?.message;
|
||||||
if (assistant) setMessages((prev) => [...prev, assistant]);
|
if (assistant) setMessages((prev) => [...prev, assistant]);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setNotification(
|
||||||
|
err?.message || 'Something went wrong while sending request',
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => setNotification(null), 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -85,6 +91,12 @@ export default function ChatUI({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 bg-primary text-primary transition-theme">
|
<div className="flex flex-1 bg-primary text-primary transition-theme">
|
||||||
|
{notification && (
|
||||||
|
<div className="fixed bottom-4 right-4 bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg z-50">
|
||||||
|
{notification}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Sidebar (icon rail + sliding panel) ── */}
|
{/* ── Sidebar (icon rail + sliding panel) ── */}
|
||||||
<Sidebar panels={SIDEBAR_PANELS({ settings, setSettings })} />
|
<Sidebar panels={SIDEBAR_PANELS({ settings, setSettings })} />
|
||||||
|
|
||||||
|
|||||||
@@ -48,32 +48,99 @@ function SettingsPanel({ settings, setSettings }: PanelProps) {
|
|||||||
(val: Settings[K]) =>
|
(val: Settings[K]) =>
|
||||||
setSettings((prev) => ({ ...prev, [key]: val }));
|
setSettings((prev) => ({ ...prev, [key]: val }));
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: 'Sampling',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Slider
|
||||||
|
label="Temperature"
|
||||||
|
value={s.temp}
|
||||||
|
setValue={set('temp')}
|
||||||
|
min={0}
|
||||||
|
max={2}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label="Top P"
|
||||||
|
value={s.topP}
|
||||||
|
setValue={set('topP')}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
/>
|
||||||
|
<NumberInput label="Top K" value={s.topK} setValue={set('topK')} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Generation',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<NumberInput
|
||||||
|
label="Max Tokens"
|
||||||
|
value={s.maxTokens}
|
||||||
|
setValue={set('maxTokens')}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Context Size"
|
||||||
|
value={s.numCtx}
|
||||||
|
setValue={set('numCtx')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Stop Sequences"
|
||||||
|
value={s.stop?.join(', ')}
|
||||||
|
setValue={(val) =>
|
||||||
|
set('stop')(val ? val.split(',').map((s) => s.trim()) : undefined)
|
||||||
|
}
|
||||||
|
placeholder="Comma separated"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Control',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Slider
|
||||||
|
label="Repeat Penalty"
|
||||||
|
value={s.repeatPenalty}
|
||||||
|
setValue={set('repeatPenalty')}
|
||||||
|
min={0}
|
||||||
|
max={2}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
<NumberInput label="Seed" value={s.seed} setValue={set('seed')} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'System',
|
||||||
|
fields: (s: any, set: any) => (
|
||||||
|
<>
|
||||||
|
<Toggle label="Streaming" value={s.stream} setValue={set('stream')} />
|
||||||
|
<TextInput
|
||||||
|
label="Keep Alive"
|
||||||
|
value={s.keepAlive}
|
||||||
|
setValue={set('keepAlive')}
|
||||||
|
placeholder="e.g. 5m"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-1">
|
<div className="space-y-6 px-1">
|
||||||
<div className="space-y-4">
|
{sections.map((section) => (
|
||||||
<h3 className="text-xs uppercase text-accent tracking-widest">Sampling</h3>
|
<div key={section.title} className="space-y-4">
|
||||||
<Slider label="Temperature" value={settings.temp} setValue={set('temp')} min={0} max={2} step={0.1} />
|
<h3 className="text-xs uppercase text-accent tracking-widest">
|
||||||
<Slider label="Top P" value={settings.topP} setValue={set('topP')} min={0} max={1} step={0.05} />
|
{section.title}
|
||||||
<NumberInput label="Top K" value={settings.topK} setValue={set('topK')} />
|
</h3>
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
{section.fields(settings, set)}
|
||||||
<h3 className="text-xs uppercase text-accent tracking-widest">Generation</h3>
|
|
||||||
<NumberInput label="Max Tokens" value={settings.maxTokens} setValue={set('maxTokens')} />
|
|
||||||
<NumberInput label="Context Size" value={settings.numCtx} setValue={set('numCtx')} />
|
|
||||||
<TextInput label="Stop Sequences" value={settings.stop?.join(', ')} setValue={(val) =>
|
|
||||||
set('stop')(val ? val.split(',').map((s) => s.trim()) : undefined)
|
|
||||||
} placeholder="Comma separated" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xs uppercase text-accent tracking-widest">Control</h3>
|
|
||||||
<Slider label="Repeat Penalty" value={settings.repeatPenalty} setValue={set('repeatPenalty')} min={0} max={2} step={0.1} />
|
|
||||||
<NumberInput label="Seed" value={settings.seed} setValue={set('seed')} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-xs uppercase text-accent tracking-widest">System</h3>
|
|
||||||
<Toggle label="Streaming" value={settings.stream} setValue={set('stream')} />
|
|
||||||
<TextInput label="Keep Alive" value={settings.keepAlive} setValue={set('keepAlive')} placeholder="e.g. 5m" />
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,7 +169,10 @@ function InfoPanel() {
|
|||||||
|
|
||||||
// ─── Panel registry — add yours here ─────────────────────────────────────────
|
// ─── Panel registry — add yours here ─────────────────────────────────────────
|
||||||
|
|
||||||
export const SIDEBAR_PANELS = ({ settings, setSettings }: PanelProps): SidebarPanel[] => [
|
export const SIDEBAR_PANELS = ({
|
||||||
|
settings,
|
||||||
|
setSettings,
|
||||||
|
}: PanelProps): SidebarPanel[] => [
|
||||||
{
|
{
|
||||||
id: 'history',
|
id: 'history',
|
||||||
label: 'History',
|
label: 'History',
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// src/components/icons/CloseIcon.tsx
|
||||||
|
export function CloseIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor" {...props}>
|
||||||
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,11 +40,7 @@ export type ChatCompletionResponse = {
|
|||||||
usage?: Usage;
|
usage?: Usage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FinishReason =
|
export type FinishReason = 'Stop' | 'Length' | 'ContentFilter' | 'ToolCalls';
|
||||||
| 'Stop'
|
|
||||||
| 'Length'
|
|
||||||
| 'ContentFilter'
|
|
||||||
| 'ToolCalls';
|
|
||||||
|
|
||||||
export type ChatChoice = {
|
export type ChatChoice = {
|
||||||
index: number;
|
index: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user