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:
co-authored by
Claude Opus 4.6
parent
a3b80b5548
commit
a9a15a7f3f
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user