feat: add parameters

This commit is contained in:
2026-04-28 16:12:53 +02:00
parent 1c7d22e04d
commit e2266b7bd7
4 changed files with 241 additions and 40 deletions
+118
View File
@@ -0,0 +1,118 @@
type SliderProps = {
label: string;
value: number;
setValue: (val: number) => void;
min: number;
max: number;
step: number;
};
export function Slider({
label,
value,
setValue,
min,
max,
step,
}: SliderProps) {
return (
<div>
<label className="text-xs uppercase tracking-widest">{label}</label>
<div className="flex items-center gap-3 mt-2">
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => setValue(Number(e.target.value))}
className="flex-1 accent-violet-400"
/>
<span className="text-sm w-10 text-right">{value}</span>
</div>
</div>
);
}
type NumberInputProps = {
label: string;
value?: number;
setValue: (val: number | undefined) => void;
};
export function NumberInput({
label,
value,
setValue,
}: NumberInputProps) {
return (
<div>
<label className="text-xs uppercase tracking-widest">{label}</label>
<input
type="number"
value={value ?? ''}
onChange={(e) =>
setValue(e.target.value ? Number(e.target.value) : undefined)
}
className="w-full mt-2 p-2 rounded bg-black/20 border border-muted"
/>
</div>
);
}
type TextInputProps = {
label: string;
value?: string;
setValue: (val: string | undefined) => void;
placeholder?: string;
};
export function TextInput({
label,
value,
setValue,
placeholder,
}: TextInputProps) {
return (
<div>
<label className="text-xs uppercase tracking-widest">{label}</label>
<input
type="text"
value={value ?? ''}
onChange={(e) => setValue(e.target.value || undefined)}
placeholder={placeholder}
className="w-full mt-2 p-2 rounded bg-black/20 border border-muted"
/>
</div>
);
}
type ToggleProps = {
label: string;
value: boolean;
setValue: (val: boolean) => void;
};
export function Toggle({
label,
value,
setValue,
}: ToggleProps) {
return (
<div className="flex items-center justify-between">
<span className="text-sm">{label}</span>
<button
onClick={() => setValue(!value)}
className={`w-10 h-5 rounded-full ${
value ? 'bg-violet-500' : 'bg-white/20'
} relative`}
>
<span
className={`absolute top-0.5 w-4 h-4 bg-white rounded-full transition-all ${
value ? 'left-5' : 'left-0.5'
}`}
/>
</button>
</div>
);
}
+32 -3
View File
@@ -6,6 +6,8 @@ import Sidebar from '../Sidebar';
import { SIDEBAR_PANELS } from './chat.panels';
import { Session } from 'next-auth';
import { Message, ModelInfo } from '@/types/chat-api';
import { ChatRequest } from '@/types/chat-api';
import { Settings } from './chat.panels';
type Props = {
session: Session;
@@ -23,6 +25,18 @@ export default function ChatUI({
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
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',
});
const sendMessage = async () => {
if (!input.trim() || !selectedModel) return;
@@ -33,6 +47,21 @@ export default function ChatUI({
setLoading(true);
try {
const body: ChatRequest = {
model: selectedModel!,
messages: newMessages,
temperature: settings.temp,
top_p: settings.topP,
top_k: settings.topK,
num_predict: settings.maxTokens,
num_ctx: settings.numCtx,
repeat_penalty: settings.repeatPenalty,
seed: settings.seed,
stream: settings.stream,
keep_alive: settings.keepAlive,
stop: settings.stop,
};
const res = await fetch(
process.env.NEXT_PUBLIC_API_URL + '/chat/completions',
{
@@ -41,7 +70,7 @@ export default function ChatUI({
'Content-Type': 'application/json',
Authorization: `Bearer ${session?.accessToken}`,
},
body: JSON.stringify({ model: selectedModel, messages: newMessages }),
body: JSON.stringify(body),
},
);
const data = await res.json();
@@ -57,7 +86,7 @@ export default function ChatUI({
return (
<div className="flex flex-1 bg-primary text-primary transition-theme">
{/* ── Sidebar (icon rail + sliding panel) ── */}
<Sidebar panels={SIDEBAR_PANELS} />
<Sidebar panels={SIDEBAR_PANELS({ settings, setSettings })} />
{/* ── Main chat area ── */}
<div className="flex-1 flex flex-col min-w-0">
@@ -71,7 +100,7 @@ export default function ChatUI({
</header>
{/* Messages */}
<main className="flex-1 overflow-y-auto flex flex-col justify-end p-6">
<main className="flex-1 overflow-y-auto flex flex-col p-6">
<div className="w-full max-w-2xl mx-auto space-y-3">
{messages.map((msg, i) => (
<div key={i} className="text-sm">
+48 -36
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { SidebarPanel } from '../Sidebar';
import { Slider, NumberInput, TextInput, Toggle } from '../Inputs';
// ─── Example panels ───────────────────────────────────────────────────────────
@@ -24,43 +24,55 @@ function ConversationHistoryPanel() {
);
}
function SettingsPanel() {
const [temp, setTemp] = useState(0.7);
const [stream, setStream] = useState(true);
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 }));
return (
<div className="space-y-4 px-1">
<div>
<label className="text-xs text-primary uppercase tracking-widest transition-theme">
Temperature
</label>
<div className="flex items-center gap-3 mt-2">
<input
type="range"
min={0}
max={2}
step={0.1}
value={temp}
onChange={(e) => setTemp(Number(e.target.value))}
className="flex-1 accent-violet-400"
/>
<span className="text-sm w-8 text-right">{temp}</span>
<div className="space-y-6 px-1">
<div className="space-y-4">
<h3 className="text-xs uppercase text-accent tracking-widest">Sampling</h3>
<Slider label="Temperature" value={settings.temp} setValue={set('temp')} min={0} max={2} step={0.1} />
<Slider label="Top P" value={settings.topP} setValue={set('topP')} min={0} max={1} step={0.05} />
<NumberInput label="Top K" value={settings.topK} setValue={set('topK')} />
</div>
<div className="space-y-4">
<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="flex items-center justify-between">
<span className="text-sm">Streaming</span>
<button
onClick={() => setStream((s) => !s)}
className={`w-10 h-5 rounded-full transition-colors ${
stream ? 'bg-violet-500' : 'bg-white/20'
} relative`}
>
<span
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${
stream ? 'left-5' : 'left-0.5'
}`}
/>
</button>
<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>
);
@@ -90,7 +102,7 @@ function InfoPanel() {
// ─── Panel registry — add yours here ─────────────────────────────────────────
export const SIDEBAR_PANELS: SidebarPanel[] = [
export const SIDEBAR_PANELS = ({ settings, setSettings }: PanelProps): SidebarPanel[] => [
{
id: 'history',
label: 'History',
@@ -117,7 +129,7 @@ export const SIDEBAR_PANELS: SidebarPanel[] = [
/>
</svg>
),
component: <SettingsPanel />,
component: <SettingsPanel settings={settings} setSettings={setSettings} />,
},
{
id: 'info',
+42
View File
@@ -15,3 +15,45 @@ export type Message = {
role: Role;
content: string;
};
export type ChatRequest = {
model: string;
stream?: boolean;
temperature?: number;
top_p?: number;
top_k?: number;
repeat_penalty?: number;
seed?: number;
num_ctx?: number;
num_predict?: number;
stop?: string[];
keep_alive?: string;
messages: Message[];
};
export type ChatCompletionResponse = {
id: string;
object: string;
created: number;
model: string;
choices: ChatChoice[];
usage?: Usage;
};
export type FinishReason =
| 'Stop'
| 'Length'
| 'ContentFilter'
| 'ToolCalls';
export type ChatChoice = {
index: number;
message: Message;
finish_reason: FinishReason;
};
export type Usage = {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};