feat: add parameters
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import Sidebar from '../Sidebar';
|
|||||||
import { SIDEBAR_PANELS } from './chat.panels';
|
import { SIDEBAR_PANELS } from './chat.panels';
|
||||||
import { Session } from 'next-auth';
|
import { Session } from 'next-auth';
|
||||||
import { Message, ModelInfo } from '@/types/chat-api';
|
import { Message, ModelInfo } from '@/types/chat-api';
|
||||||
|
import { ChatRequest } from '@/types/chat-api';
|
||||||
|
import { Settings } from './chat.panels';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
session: Session;
|
session: Session;
|
||||||
@@ -23,6 +25,18 @@ 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 [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 () => {
|
const sendMessage = async () => {
|
||||||
if (!input.trim() || !selectedModel) return;
|
if (!input.trim() || !selectedModel) return;
|
||||||
@@ -33,6 +47,21 @@ export default function ChatUI({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
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(
|
const res = await fetch(
|
||||||
process.env.NEXT_PUBLIC_API_URL + '/chat/completions',
|
process.env.NEXT_PUBLIC_API_URL + '/chat/completions',
|
||||||
{
|
{
|
||||||
@@ -41,7 +70,7 @@ export default function ChatUI({
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${session?.accessToken}`,
|
Authorization: `Bearer ${session?.accessToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ model: selectedModel, messages: newMessages }),
|
body: JSON.stringify(body),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -57,7 +86,7 @@ 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">
|
||||||
{/* ── Sidebar (icon rail + sliding panel) ── */}
|
{/* ── Sidebar (icon rail + sliding panel) ── */}
|
||||||
<Sidebar panels={SIDEBAR_PANELS} />
|
<Sidebar panels={SIDEBAR_PANELS({ settings, setSettings })} />
|
||||||
|
|
||||||
{/* ── Main chat area ── */}
|
{/* ── Main chat area ── */}
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
@@ -71,7 +100,7 @@ export default function ChatUI({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* 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">
|
<div className="w-full max-w-2xl mx-auto space-y-3">
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => (
|
||||||
<div key={i} className="text-sm">
|
<div key={i} className="text-sm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { SidebarPanel } from '../Sidebar';
|
import { SidebarPanel } from '../Sidebar';
|
||||||
|
import { Slider, NumberInput, TextInput, Toggle } from '../Inputs';
|
||||||
|
|
||||||
// ─── Example panels ───────────────────────────────────────────────────────────
|
// ─── Example panels ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -24,43 +24,55 @@ function ConversationHistoryPanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsPanel() {
|
type PanelProps = {
|
||||||
const [temp, setTemp] = useState(0.7);
|
settings: Settings;
|
||||||
const [stream, setStream] = useState(true);
|
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 (
|
return (
|
||||||
<div className="space-y-4 px-1">
|
<div className="space-y-6 px-1">
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<label className="text-xs text-primary uppercase tracking-widest transition-theme">
|
<h3 className="text-xs uppercase text-accent tracking-widest">Sampling</h3>
|
||||||
Temperature
|
<Slider label="Temperature" value={settings.temp} setValue={set('temp')} min={0} max={2} step={0.1} />
|
||||||
</label>
|
<Slider label="Top P" value={settings.topP} setValue={set('topP')} min={0} max={1} step={0.05} />
|
||||||
<div className="flex items-center gap-3 mt-2">
|
<NumberInput label="Top K" value={settings.topK} setValue={set('topK')} />
|
||||||
<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>
|
</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>
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<span className="text-sm">Streaming</span>
|
<h3 className="text-xs uppercase text-accent tracking-widest">Control</h3>
|
||||||
<button
|
<Slider label="Repeat Penalty" value={settings.repeatPenalty} setValue={set('repeatPenalty')} min={0} max={2} step={0.1} />
|
||||||
onClick={() => setStream((s) => !s)}
|
<NumberInput label="Seed" value={settings.seed} setValue={set('seed')} />
|
||||||
className={`w-10 h-5 rounded-full transition-colors ${
|
</div>
|
||||||
stream ? 'bg-violet-500' : 'bg-white/20'
|
<div className="space-y-4">
|
||||||
} relative`}
|
<h3 className="text-xs uppercase text-accent tracking-widest">System</h3>
|
||||||
>
|
<Toggle label="Streaming" value={settings.stream} setValue={set('stream')} />
|
||||||
<span
|
<TextInput label="Keep Alive" value={settings.keepAlive} setValue={set('keepAlive')} placeholder="e.g. 5m" />
|
||||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${
|
|
||||||
stream ? 'left-5' : 'left-0.5'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -90,7 +102,7 @@ function InfoPanel() {
|
|||||||
|
|
||||||
// ─── Panel registry — add yours here ─────────────────────────────────────────
|
// ─── Panel registry — add yours here ─────────────────────────────────────────
|
||||||
|
|
||||||
export const SIDEBAR_PANELS: SidebarPanel[] = [
|
export const SIDEBAR_PANELS = ({ settings, setSettings }: PanelProps): SidebarPanel[] => [
|
||||||
{
|
{
|
||||||
id: 'history',
|
id: 'history',
|
||||||
label: 'History',
|
label: 'History',
|
||||||
@@ -117,7 +129,7 @@ export const SIDEBAR_PANELS: SidebarPanel[] = [
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
component: <SettingsPanel />,
|
component: <SettingsPanel settings={settings} setSettings={setSettings} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'info',
|
id: 'info',
|
||||||
|
|||||||
@@ -15,3 +15,45 @@ export type Message = {
|
|||||||
role: Role;
|
role: Role;
|
||||||
content: string;
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user