feat(chat): add temperature/params sliders & stop sequences (M9.9, M9.10)

Collapsible "Advanced" panel with Temperature, Max Tokens, Top-P sliders
and stop sequence tag input. Params persisted per conversation and passed
to Claude API. Reset to defaults button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 23:40:32 +00:00
co-authored by Claude Opus 4.6
parent a3b80b5548
commit a9a15a7f3f
4 changed files with 225 additions and 9 deletions
@@ -0,0 +1,179 @@
<template>
<div class="px-3 md:px-4 pb-1">
<button
class="flex items-center gap-1.5 text-[11px] text-white/40 hover:text-white/60 transition-colors"
@click="isExpanded = !isExpanded"
>
<svg
class="w-3 h-3 transition-transform duration-200"
:class="isExpanded ? 'rotate-90' : ''"
fill="currentColor"
viewBox="0 0 20 20"
>
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
Advanced
</button>
<div v-if="isExpanded && conv" class="mt-2 space-y-3 animate-fade-up-fast">
<!-- Temperature -->
<div class="space-y-1">
<div class="flex items-center justify-between">
<label class="text-[11px] text-white/40">Temperature</label>
<span class="text-[11px] text-white/50 tabular-nums">{{ temperature.toFixed(2) }}</span>
</div>
<input
v-model.number="temperature"
type="range"
min="0"
max="1"
step="0.05"
class="w-full h-1 accent-[#F7931A] bg-white/10 rounded-full appearance-none cursor-pointer"
@input="persistParams"
/>
</div>
<!-- Max Tokens -->
<div class="space-y-1">
<div class="flex items-center justify-between">
<label class="text-[11px] text-white/40">Max Tokens</label>
<span class="text-[11px] text-white/50 tabular-nums">{{ maxTokens }}</span>
</div>
<input
v-model.number="maxTokens"
type="range"
min="256"
max="8192"
step="256"
class="w-full h-1 accent-[#F7931A] bg-white/10 rounded-full appearance-none cursor-pointer"
@input="persistParams"
/>
</div>
<!-- Top P -->
<div class="space-y-1">
<div class="flex items-center justify-between">
<label class="text-[11px] text-white/40">Top P</label>
<span class="text-[11px] text-white/50 tabular-nums">{{ topP.toFixed(2) }}</span>
</div>
<input
v-model.number="topP"
type="range"
min="0"
max="1"
step="0.05"
class="w-full h-1 accent-[#F7931A] bg-white/10 rounded-full appearance-none cursor-pointer"
@input="persistParams"
/>
</div>
<!-- Stop Sequences (M9.10) -->
<div class="space-y-1">
<label class="text-[11px] text-white/40">Stop Sequences</label>
<div v-if="stopSequences.length > 0" class="flex gap-1 flex-wrap mb-1">
<span
v-for="(seq, i) in stopSequences"
:key="i"
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-white/5 border border-white/10 text-[10px] text-white/50"
>
{{ seq }}
<button class="text-white/30 hover:text-white/60" @click="removeStopSequence(i)">×</button>
</span>
</div>
<input
v-model="newStopSeq"
type="text"
class="w-full bg-white/5 border border-white/10 rounded-lg px-2.5 py-1.5 text-xs text-white/80 outline-none focus:border-accent/40 placeholder:text-white/25"
placeholder="Add stop sequence (Enter to add)"
@keydown.enter="addStopSequence"
/>
</div>
<!-- Reset -->
<button
class="text-[11px] text-white/30 hover:text-white/50 transition-colors"
@click="resetDefaults"
>
Reset to defaults
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useChatStore } from '@/stores/chat'
const chatStore = useChatStore()
const isExpanded = ref(false)
const newStopSeq = ref('')
const conv = computed(() => chatStore.activeConversation)
const temperature = ref(1.0)
const maxTokens = ref(4096)
const topP = ref(1.0)
const stopSequences = ref<string[]>([])
// Sync from conversation on switch
watch(
() => chatStore.activeConversationId,
() => loadFromConv(),
{ immediate: true }
)
function loadFromConv() {
const c = conv.value
temperature.value = c?.temperature ?? 1.0
maxTokens.value = c?.maxTokens ?? 4096
topP.value = c?.topP ?? 1.0
stopSequences.value = c?.stopSequences ? [...c.stopSequences] : []
}
function persistParams() {
const c = conv.value
if (!c) return
c.temperature = temperature.value
c.maxTokens = maxTokens.value
c.topP = topP.value
c.updatedAt = Date.now()
}
function addStopSequence() {
const seq = newStopSeq.value.trim()
if (!seq) return
stopSequences.value.push(seq)
newStopSeq.value = ''
const c = conv.value
if (c) {
c.stopSequences = [...stopSequences.value]
c.updatedAt = Date.now()
}
}
function removeStopSequence(index: number) {
stopSequences.value.splice(index, 1)
const c = conv.value
if (c) {
c.stopSequences = [...stopSequences.value]
c.updatedAt = Date.now()
}
}
function resetDefaults() {
temperature.value = 1.0
maxTokens.value = 4096
topP.value = 1.0
stopSequences.value = []
newStopSeq.value = ''
const c = conv.value
if (c) {
c.temperature = undefined
c.maxTokens = undefined
c.topP = undefined
c.stopSequences = undefined
c.updatedAt = Date.now()
}
}
</script>
@@ -22,6 +22,7 @@
<PersonaSelector />
<MemoryPanel />
<AdvancedSettings />
<!-- Collapsed: prompt index -->
<PromptIndex
@@ -122,6 +123,7 @@ import ContextBar from './ContextBar.vue'
import ComparisonView from './ComparisonView.vue'
import PersonaSelector from './PersonaSelector.vue'
import MemoryPanel from './MemoryPanel.vue'
import AdvancedSettings from './AdvancedSettings.vue'
import ErrorBoundary from '@/components/ui/ErrorBoundary.vue'
import type { Message, ImageAttachment } from '@aiui/core/types/message'
+38 -9
View File
@@ -149,6 +149,13 @@ async function streamMock(
}
}
interface GenerationParams {
temperature?: number
maxTokens?: number
topP?: number
stopSequences?: string[]
}
async function streamClaude(
messages: ChatMessage[],
onToken: (text: string) => void,
@@ -156,6 +163,7 @@ async function streamClaude(
systemPrompt: string,
webSearch: boolean,
signal?: AbortSignal,
params?: GenerationParams,
): Promise<void> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
@@ -171,16 +179,22 @@ async function streamClaude(
content: buildClaudeContent(m),
}))
const body: Record<string, unknown> = {
model: activeModel.value,
system: systemPrompt,
messages: apiMessages,
stream: true,
webSearch,
}
if (params?.temperature !== undefined) body.temperature = params.temperature
if (params?.maxTokens !== undefined) body.max_tokens = params.maxTokens
if (params?.topP !== undefined) body.top_p = params.topP
if (params?.stopSequences && params.stopSequences.length > 0) body.stop_sequences = params.stopSequences
const res = await fetch(CLAUDE_PATH, {
method: 'POST',
headers,
body: JSON.stringify({
model: activeModel.value,
system: systemPrompt,
messages: apiMessages,
stream: true,
webSearch,
}),
body: JSON.stringify(body),
signal,
})
@@ -332,6 +346,17 @@ function buildSystemPrompt(chatStore: ReturnType<typeof useChatStore>): string {
return prompt
}
function getConversationParams(chatStore: ReturnType<typeof useChatStore>): GenerationParams {
const conv = chatStore.activeConversation
if (!conv) return {}
return {
temperature: conv.temperature,
maxTokens: conv.maxTokens,
topP: conv.topP,
stopSequences: conv.stopSequences,
}
}
let currentAbort: AbortController | null = null
/** Background title generation after first exchange */
@@ -459,9 +484,11 @@ export function useAI() {
chatStore.appendToLastMessage(cid, `${err}`)
}
const genParams = getConversationParams(chatStore)
try {
if (provider === 'claude') {
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal)
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal, genParams)
} else if (provider === 'openrouter') {
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
} else {
@@ -565,9 +592,11 @@ export function useAI() {
chatStore.appendToLastMessage(cid, `${err}`)
}
const genParams = getConversationParams(chatStore)
try {
if (provider === 'claude') {
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal)
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal, genParams)
} else if (provider === 'openrouter') {
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
} else {
+6
View File
@@ -58,4 +58,10 @@ export interface Conversation {
childBranchIds?: string[]
/** ID of the persona applied to this conversation */
personaId?: string
/** Generation params (persisted per conversation) */
temperature?: number
maxTokens?: number
topP?: number
/** Comma-separated stop sequences */
stopSequences?: string[]
}