feat(chat): add system prompt editor & personas (M9.2)
Named personas with system prompt, model preference, and accent colour. Persona pill selector above chat input, editor modal for create/edit/delete. Default persona auto-applied to new conversations. Persona system prompt prepended to AI context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
27aeb2b188
commit
77b985db9c
@@ -168,7 +168,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
Array.from(conversations.value.values()).sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
)
|
||||
|
||||
function createConversation(title = 'New Chat'): string {
|
||||
function createConversation(title = 'New Chat', personaId?: string): string {
|
||||
const id = generateId()
|
||||
const conversation: Conversation = {
|
||||
id,
|
||||
@@ -176,6 +176,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
personaId,
|
||||
}
|
||||
conversations.value.set(id, conversation)
|
||||
activeConversationId.value = id
|
||||
@@ -266,6 +267,72 @@ export const useChatStore = defineStore('chat', () => {
|
||||
debouncedIDBSave(conv)
|
||||
}
|
||||
|
||||
/** Branch from a message — creates a new conversation with messages up to (and including) the given message */
|
||||
function branchFromMessage(conversationId: string, messageId: string): string | null {
|
||||
const conv = conversations.value.get(conversationId)
|
||||
if (!conv) return null
|
||||
|
||||
const msgIndex = conv.messages.findIndex((m: { id: string }) => m.id === messageId)
|
||||
if (msgIndex === -1) return null
|
||||
|
||||
const branchId = generateId()
|
||||
const branchMessages = conv.messages.slice(0, msgIndex + 1).map(m => ({
|
||||
...m,
|
||||
id: generateId(),
|
||||
timestamp: m.timestamp,
|
||||
}))
|
||||
|
||||
// Count existing branches to label them
|
||||
const existingBranches = (conv.childBranchIds ?? []).length
|
||||
const branchConv: Conversation = {
|
||||
id: branchId,
|
||||
title: `${conv.title} (Branch ${existingBranches + 2})`,
|
||||
messages: branchMessages,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
model: conv.model,
|
||||
systemPrompt: conv.systemPrompt,
|
||||
parentConversationId: conversationId,
|
||||
branchPoint: messageId,
|
||||
}
|
||||
|
||||
conversations.value.set(branchId, branchConv)
|
||||
debouncedIDBSave(branchConv)
|
||||
|
||||
// Track branch in parent
|
||||
if (!conv.childBranchIds) conv.childBranchIds = []
|
||||
conv.childBranchIds.push(branchId)
|
||||
conv.updatedAt = Date.now()
|
||||
debouncedIDBSave(conv)
|
||||
|
||||
activeConversationId.value = branchId
|
||||
return branchId
|
||||
}
|
||||
|
||||
/** Get sibling branches for a conversation (parent + all its children) */
|
||||
function getSiblingBranches(conversationId: string): { id: string; title: string; isCurrent: boolean }[] {
|
||||
const conv = conversations.value.get(conversationId)
|
||||
if (!conv) return []
|
||||
|
||||
// Find the family (parent + children)
|
||||
const parentId = conv.parentConversationId ?? conversationId
|
||||
const parent = conversations.value.get(parentId)
|
||||
if (!parent) return []
|
||||
|
||||
const siblings: { id: string; title: string; isCurrent: boolean }[] = [
|
||||
{ id: parentId, title: parent.title, isCurrent: parentId === conversationId },
|
||||
]
|
||||
|
||||
for (const childId of parent.childBranchIds ?? []) {
|
||||
const child = conversations.value.get(childId)
|
||||
if (child) {
|
||||
siblings.push({ id: childId, title: child.title, isCurrent: childId === conversationId })
|
||||
}
|
||||
}
|
||||
|
||||
return siblings
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
activeConversationId,
|
||||
@@ -287,5 +354,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
deleteConversation,
|
||||
updateMessageContent,
|
||||
deleteMessagesAfter,
|
||||
branchFromMessage,
|
||||
getSiblingBranches,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface Persona {
|
||||
id: string
|
||||
name: string
|
||||
systemPrompt: string
|
||||
modelPreference?: string
|
||||
accentColor?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'aiui-personas'
|
||||
|
||||
function generateId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
function loadFromStorage(): Persona[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveToStorage(personas: Persona[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(personas))
|
||||
}
|
||||
|
||||
export const usePersonaStore = defineStore('personas', () => {
|
||||
const personas = ref<Persona[]>(loadFromStorage())
|
||||
|
||||
const defaultPersona = computed(() => personas.value.find(p => p.isDefault) ?? null)
|
||||
|
||||
const sortedPersonas = computed(() =>
|
||||
[...personas.value].sort((a, b) => {
|
||||
if (a.isDefault && !b.isDefault) return -1
|
||||
if (!a.isDefault && b.isDefault) return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
)
|
||||
|
||||
function persist() {
|
||||
saveToStorage(personas.value)
|
||||
}
|
||||
|
||||
function addPersona(data: Omit<Persona, 'id'>): Persona {
|
||||
const persona: Persona = { ...data, id: generateId() }
|
||||
// If this is the first or marked default, clear other defaults
|
||||
if (persona.isDefault) {
|
||||
personas.value.forEach(p => { p.isDefault = false })
|
||||
}
|
||||
personas.value.push(persona)
|
||||
persist()
|
||||
return persona
|
||||
}
|
||||
|
||||
function updatePersona(id: string, data: Partial<Omit<Persona, 'id'>>) {
|
||||
const p = personas.value.find(x => x.id === id)
|
||||
if (!p) return
|
||||
if (data.isDefault) {
|
||||
personas.value.forEach(x => { x.isDefault = false })
|
||||
}
|
||||
Object.assign(p, data)
|
||||
persist()
|
||||
}
|
||||
|
||||
function deletePersona(id: string) {
|
||||
const idx = personas.value.findIndex(x => x.id === id)
|
||||
if (idx !== -1) {
|
||||
personas.value.splice(idx, 1)
|
||||
persist()
|
||||
}
|
||||
}
|
||||
|
||||
function getPersona(id: string): Persona | undefined {
|
||||
return personas.value.find(x => x.id === id)
|
||||
}
|
||||
|
||||
return {
|
||||
personas,
|
||||
sortedPersonas,
|
||||
defaultPersona,
|
||||
addPersona,
|
||||
updatePersona,
|
||||
deletePersona,
|
||||
getPersona,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user