Format system prompt
This commit is contained in:
parent
c8058f8663
commit
c2c55eb820
|
|
@ -188,6 +188,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.stopButton {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: var(--bg);
|
||||
background: var(--error, #f44336);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { useAppState, type Character } from "../contexts/state";
|
||||
import { useState } from "preact/hooks";
|
||||
import styles from '../assets/character-editor.module.css';
|
||||
import LLM from "../utils/llm";
|
||||
|
||||
export const CharacterEditor = () => {
|
||||
const { currentStory, dispatch } = useAppState();
|
||||
const { currentStory, dispatch, connection, model } = useAppState();
|
||||
const [newNickname, setNewNickname] = useState<Record<string, string>>({});
|
||||
const [newRelation, setNewRelation] = useState<Record<string, { name: string; relation: string }>>({});
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
||||
|
||||
if (!currentStory) {
|
||||
return null;
|
||||
|
|
@ -99,6 +101,23 @@ export const CharacterEditor = () => {
|
|||
setNewRelation({ ...newRelation, [characterId]: { ...current, [field]: value } });
|
||||
};
|
||||
|
||||
const handleGenerateShortDescription = async (characterId: string) => {
|
||||
if (!connection || !model) return;
|
||||
|
||||
const character = currentStory.characters.find(c => c.id === characterId);
|
||||
if (!character || !character.description.trim()) return;
|
||||
|
||||
setGeneratingShortDesc(characterId);
|
||||
try {
|
||||
const shortDesc = await LLM.summarize(connection, model.id, character.description, 'sentence');
|
||||
handleEditCharacter(characterId, 'shortDescription', shortDesc.trim());
|
||||
} catch (error) {
|
||||
console.error('Failed to generate short description:', error);
|
||||
} finally {
|
||||
setGeneratingShortDesc(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={styles.characterEditor}>
|
||||
<div class={styles.header}>
|
||||
|
|
@ -167,8 +186,12 @@ export const CharacterEditor = () => {
|
|||
<div class={styles.field}>
|
||||
<div class={styles.label}>
|
||||
Short Description
|
||||
<button class={styles.generateButton}>
|
||||
Generate
|
||||
<button
|
||||
class={styles.generateButton}
|
||||
onClick={() => handleGenerateShortDescription(character.id)}
|
||||
disabled={!character.description.trim() || generatingShortDesc === character.id || !connection || !model}
|
||||
>
|
||||
{generatingShortDesc === character.id ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
|
|
|
|||
|
|
@ -151,6 +151,9 @@ export const ChatSidebar = () => {
|
|||
let tool_calls: LLM.ToolCall[] | undefined;
|
||||
|
||||
for await (const chunk of LLM.generateStream(connection, request)) {
|
||||
if (abortControllerRef.current?.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
|
|
@ -178,9 +181,6 @@ export const ChatSidebar = () => {
|
|||
},
|
||||
});
|
||||
}
|
||||
if (abortControllerRef.current?.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: assistantMessageId,
|
||||
|
|
@ -236,15 +236,20 @@ export const ChatSidebar = () => {
|
|||
setInput('');
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
await sendMessage([userMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
abortControllerRef.current = new AbortController();
|
||||
}
|
||||
}, [currentStory, input, connection, model, isLoading, sendMessage]);
|
||||
|
||||
const handleStopGeneration = useCallback(() => {
|
||||
abortControllerRef.current?.abort();
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
|
@ -346,13 +351,22 @@ export const ChatSidebar = () => {
|
|||
rows={3}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<button
|
||||
class={styles.sendButton}
|
||||
onClick={handleSendMessage}
|
||||
disabled={isDisabled || !input.trim()}
|
||||
>
|
||||
{isLoading ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
{isLoading ? (
|
||||
<button
|
||||
class={styles.stopButton}
|
||||
onClick={handleStopGeneration}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
class={styles.sendButton}
|
||||
onClick={handleSendMessage}
|
||||
disabled={isDisabled || !input.trim()}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useAppState, type Location, LocationScale } from "../contexts/state";
|
||||
import { useState } from "preact/hooks";
|
||||
import styles from '../assets/location-editor.module.css';
|
||||
import LLM from "../utils/llm";
|
||||
|
||||
const SCALE_OPTIONS = Object.entries(LocationScale)
|
||||
.filter(([, value]) => typeof value === 'number')
|
||||
|
|
@ -10,8 +11,9 @@ const SCALE_OPTIONS = Object.entries(LocationScale)
|
|||
}));
|
||||
|
||||
export const LocationEditor = () => {
|
||||
const { currentStory, dispatch } = useAppState();
|
||||
const { currentStory, dispatch, connection, model } = useAppState();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
|
||||
|
||||
if (!currentStory) {
|
||||
return null;
|
||||
|
|
@ -48,6 +50,23 @@ export const LocationEditor = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleGenerateShortDescription = async (locationId: string) => {
|
||||
if (!connection || !model) return;
|
||||
|
||||
const location = currentStory.locations.find(l => l.id === locationId);
|
||||
if (!location || !location.description.trim()) return;
|
||||
|
||||
setGeneratingShortDesc(locationId);
|
||||
try {
|
||||
const shortDesc = await LLM.summarize(connection, model.id, location.description, 'sentence');
|
||||
handleEditLocation(locationId, 'shortDescription', shortDesc.trim());
|
||||
} catch (error) {
|
||||
console.error('Failed to generate short description:', error);
|
||||
} finally {
|
||||
setGeneratingShortDesc(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={styles.locationEditor}>
|
||||
<div class={styles.header}>
|
||||
|
|
@ -131,8 +150,12 @@ export const LocationEditor = () => {
|
|||
<div class={styles.field}>
|
||||
<div class={styles.label}>
|
||||
Short Description
|
||||
<button class={styles.generateButton}>
|
||||
Generate
|
||||
<button
|
||||
class={styles.generateButton}
|
||||
onClick={() => handleGenerateShortDescription(location.id)}
|
||||
disabled={!location.description.trim() || generatingShortDesc === location.id || !connection || !model}
|
||||
>
|
||||
{generatingShortDesc === location.id ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ interface IState {
|
|||
model: LLM.ModelInfo | null;
|
||||
enableThinking: boolean;
|
||||
bannedTokens: string[];
|
||||
systemInstruction: string;
|
||||
}
|
||||
|
||||
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -74,6 +75,7 @@ type Action =
|
|||
| { type: 'RENAME_STORY'; id: string; title: string }
|
||||
| { type: 'EDIT_STORY'; id: string; text: string }
|
||||
| { type: 'EDIT_LORE'; id: string; lore: string }
|
||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
|
||||
| { type: 'DELETE_STORY'; id: string }
|
||||
| { type: 'SELECT_STORY'; id: string }
|
||||
|
|
@ -102,6 +104,7 @@ const DEFAULT_STATE: IState = {
|
|||
model: null,
|
||||
enableThinking: false,
|
||||
bannedTokens: [],
|
||||
systemInstruction: `You are a creative writing assistant. Help the user develop their story by writing engaging content, maintaining consistency with the established characters, settings, and plot. Follow the user's instructions while staying true to the story's tone and style.`,
|
||||
};
|
||||
|
||||
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -149,6 +152,12 @@ function reducer(state: IState, action: Action): IState {
|
|||
),
|
||||
};
|
||||
}
|
||||
case 'SET_SYSTEM_INSTRUCTION': {
|
||||
return {
|
||||
...state,
|
||||
systemInstruction: action.systemInstruction,
|
||||
};
|
||||
}
|
||||
case 'SET_CURRENT_TAB': {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -365,6 +374,7 @@ export interface AppState {
|
|||
model: LLM.ModelInfo | null;
|
||||
enableThinking: boolean;
|
||||
bannedTokens: string[];
|
||||
systemInstruction: string;
|
||||
dispatch: (action: Action) => void;
|
||||
}
|
||||
|
||||
|
|
@ -384,6 +394,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
|||
model: state.model,
|
||||
enableThinking: state.enableThinking,
|
||||
bannedTokens: state.bannedTokens ?? [],
|
||||
systemInstruction: state.systemInstruction ?? '',
|
||||
dispatch,
|
||||
}), [state]);
|
||||
|
||||
|
|
|
|||
|
|
@ -276,6 +276,39 @@ namespace LLM {
|
|||
export async function generate(connection: Connection, config: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
||||
return request<ChatCompletionResponse>(connection, '/v1/chat/completions', 'POST', config);
|
||||
}
|
||||
|
||||
const SUMMARIZATION_PROMPT = `Summarize the following text concisely while preserving key information and meaning. {level}
|
||||
|
||||
Text:
|
||||
{text}
|
||||
|
||||
Provide a clear and coherent summary:`;
|
||||
|
||||
export type SummarizationLevel = 'sentence' | 'paragraph' | 'arbitrary';
|
||||
const LEVEL_INSTRUCTIONS: Record<SummarizationLevel, string> = {
|
||||
sentence: 'Summarize in exactly one sentence.',
|
||||
paragraph: 'Summarize in exactly one paragraph (2-4 sentences).',
|
||||
arbitrary: 'Summarize in a way you think is appropriate for the text length and complexity.',
|
||||
};
|
||||
|
||||
export async function summarize(connection: Connection, model: string, text: string, level: SummarizationLevel = 'arbitrary'): Promise<string> {
|
||||
|
||||
const prompt = SUMMARIZATION_PROMPT
|
||||
.replace('{text}', text)
|
||||
.replace('{level}', LEVEL_INSTRUCTIONS[level]);
|
||||
|
||||
const response = await generate(connection, {
|
||||
model,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
}],
|
||||
temperature: 0.3,
|
||||
max_tokens: 500,
|
||||
});
|
||||
|
||||
return response.choices[0]?.message.content ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
export default LLM;
|
||||
|
|
@ -1,8 +1,85 @@
|
|||
import LLM from "./llm";
|
||||
import type { AppState } from "../contexts/state";
|
||||
import { type AppState, LocationScale } from "../contexts/state";
|
||||
import { Tools } from "./tools";
|
||||
|
||||
namespace Prompt {
|
||||
export function formatCharactersMarkdown(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory || !currentStory.characters?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('## Characters\n');
|
||||
|
||||
for (const character of currentStory.characters) {
|
||||
lines.push(`### ${character.name}`);
|
||||
|
||||
const description = character.shortDescription || character.description;
|
||||
if (description) {
|
||||
lines.push(description);
|
||||
}
|
||||
|
||||
if (character.relations?.length) {
|
||||
lines.push('**Relations:**');
|
||||
for (const relation of character.relations) {
|
||||
lines.push(`- ${relation.name}: ${relation.relation}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatLocationsMarkdown(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory || !currentStory.locations?.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('## Locations\n');
|
||||
|
||||
for (const location of currentStory.locations) {
|
||||
lines.push(`### ${location.name}`);
|
||||
|
||||
const description = location.shortDescription || location.description;
|
||||
if (description) {
|
||||
lines.push(description);
|
||||
}
|
||||
|
||||
lines.push(`**Scale:** ${LocationScale[location.scale]}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function formatSystemPrompt(state: AppState): string {
|
||||
const { currentStory } = state;
|
||||
if (!currentStory) {
|
||||
return state.systemInstruction;
|
||||
}
|
||||
|
||||
const parts: string[] = [state.systemInstruction];
|
||||
|
||||
parts.push(`# ${currentStory.title}`);
|
||||
|
||||
const charactersSection = formatCharactersMarkdown(state);
|
||||
if (charactersSection) {
|
||||
parts.push(charactersSection);
|
||||
}
|
||||
|
||||
const locationsSection = formatLocationsMarkdown(state);
|
||||
if (locationsSection) {
|
||||
parts.push(locationsSection);
|
||||
}
|
||||
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
export function compilePrompt(state: AppState, newMessages: LLM.ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
||||
const { currentStory, model, enableThinking } = state;
|
||||
|
||||
|
|
@ -11,7 +88,7 @@ namespace Prompt {
|
|||
}
|
||||
|
||||
const messages: LLM.ChatMessage[] = [
|
||||
// TODO system prompt
|
||||
{ role: 'system', content: formatSystemPrompt(state) },
|
||||
// TODO part of story
|
||||
...currentStory.chatMessages,
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue