Fix messages regen
This commit is contained in:
parent
12214dd2df
commit
0f326ac6ea
|
|
@ -19,11 +19,20 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleWorld {
|
||||
color: var(--text-muted);
|
||||
font-weight: normal;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titleSep {
|
||||
|
|
@ -151,6 +160,15 @@
|
|||
}
|
||||
|
||||
.title {
|
||||
padding: 0 16px 16px;
|
||||
padding: 0 16px 12px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.titleSep {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,10 +129,14 @@ export const ChatPanel = () => {
|
|||
return () => clearTimeout(timeoutId);
|
||||
}, [currentStory, connection, model, input, currentStory?.chatMessages.length]);
|
||||
|
||||
const sendMessage = useCallback(async (newMessages: ChatMessage[]) => {
|
||||
const sendMessage = useCallback(async (
|
||||
newMessages: Iterable<ChatMessage>,
|
||||
excludedMessageIds: string[] = [],
|
||||
) => {
|
||||
if (!currentStory || !currentWorld || !connection || !model) return;
|
||||
|
||||
for (const message of newMessages) {
|
||||
if (excludedMessageIds.includes(message.id)) continue;
|
||||
dispatch({
|
||||
type: 'ADD_CHAT_MESSAGE',
|
||||
worldId: currentWorld.id,
|
||||
|
|
@ -154,7 +158,7 @@ export const ChatPanel = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const request = Prompt.compilePrompt(appStateRef.current, newMessages);
|
||||
const request = Prompt.compilePrompt(appStateRef.current, newMessages, excludedMessageIds);
|
||||
|
||||
if (!request) {
|
||||
setError('Failed to compile prompt');
|
||||
|
|
@ -345,7 +349,7 @@ export const ChatPanel = () => {
|
|||
|
||||
const handleRegenerate = useCallback(async () => {
|
||||
if (!currentStory || !connection || !model || isLoading) return;
|
||||
|
||||
|
||||
// Only regenerate if last message is assistant
|
||||
const lastMessage = currentStory.chatMessages.at(-1);
|
||||
if (!lastMessage || lastMessage.role !== 'assistant') return;
|
||||
|
|
@ -363,9 +367,7 @@ export const ChatPanel = () => {
|
|||
messageId: lastMessage.id,
|
||||
});
|
||||
|
||||
// Find the message history before the deleted assistant message
|
||||
const messages = currentStory.chatMessages.slice(0, -1);
|
||||
await sendMessage(messages);
|
||||
await sendMessage([], [lastMessage.id]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -389,16 +391,15 @@ export const ChatPanel = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div class={styles.messages} ref={messagesRef}>
|
||||
{currentStory.chatMessages.map((message, index) => {
|
||||
{currentStory.chatMessages.map((message) => {
|
||||
const isEditing = editingMessageId === message.id;
|
||||
const canEdit = message.role === 'user' || message.role === 'assistant';
|
||||
const canDelete = index < currentStory.chatMessages.length - 1 || message.role === 'user' || message.role === 'assistant';
|
||||
|
||||
return (
|
||||
<div key={message.id} class={styles.message} data-role={message.role}>
|
||||
<div class={styles.messageHeader}>
|
||||
<RoleHeader message={message} chatMessages={currentStory.chatMessages} />
|
||||
|
||||
|
||||
{!isLoading && canEdit && (
|
||||
<div class={styles.messageActions}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ export const Menu = () => {
|
|||
reader.onload = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(reader.result as string);
|
||||
if (CharacterCard.isV2(parsed)) {
|
||||
if (CharacterCard.isCardData(parsed)) {
|
||||
dispatch({ type: 'IMPORT_WORLD', world: CharacterCard.toWorld(parsed) });
|
||||
} else if (isWorld(parsed)) {
|
||||
dispatch({ type: 'IMPORT_WORLD', world: parsed });
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { World, Story } from "../contexts/state";
|
|||
// ─── Spec Types ───────────────────────────────────────────────────────────────
|
||||
|
||||
namespace CharacterCard {
|
||||
export interface V2Data {
|
||||
export interface CharaCardData {
|
||||
name: string;
|
||||
description: string;
|
||||
personality: string;
|
||||
|
|
@ -19,19 +19,20 @@ namespace CharacterCard {
|
|||
character_version?: string;
|
||||
}
|
||||
|
||||
export interface V2 {
|
||||
export interface CharaCard {
|
||||
spec: "chara_card_v2";
|
||||
spec_version: string;
|
||||
data: V2Data;
|
||||
data: CharaCardData;
|
||||
}
|
||||
|
||||
// ─── Type Guard ───────────────────────────────────────────────────────────
|
||||
|
||||
export function isV2(obj: unknown): obj is V2 {
|
||||
export function isCardData(obj: unknown): obj is CharaCard {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
const c = obj as Record<string, unknown>;
|
||||
return (
|
||||
c.spec === 'chara_card_v2' &&
|
||||
typeof c.spec === 'string' &&
|
||||
['chara_card_v2', 'chara_card_v3'].includes(c.spec) &&
|
||||
typeof c.spec_version === 'string' &&
|
||||
typeof c.data === 'object' && c.data !== null &&
|
||||
typeof (c.data as Record<string, unknown>).name === 'string'
|
||||
|
|
@ -44,7 +45,7 @@ namespace CharacterCard {
|
|||
* Extracts and parses the chara_card_v2 JSON embedded in a PNG tEXt chunk.
|
||||
* Returns null if the file is not a valid V2 character card.
|
||||
*/
|
||||
export async function extractFromPng(file: File): Promise<V2 | null> {
|
||||
export async function extractFromPng(file: File): Promise<CharaCard | null> {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
|
||||
|
|
@ -79,8 +80,8 @@ namespace CharacterCard {
|
|||
|
||||
try {
|
||||
const card = JSON.parse(json);
|
||||
if (card?.spec === 'chara_card_v2' && card?.data) {
|
||||
return card as V2;
|
||||
if (['chara_card_v2', 'chara_card_v3'].includes(card?.spec) && card?.data) {
|
||||
return card as CharaCard;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -99,49 +100,57 @@ namespace CharacterCard {
|
|||
function substituteVars(text: string, charName: string): string {
|
||||
return text
|
||||
.replaceAll('{{char}}', charName)
|
||||
.replaceAll('{{Char}}', charName)
|
||||
.replaceAll('{{user}}', 'User')
|
||||
.replaceAll('{{User}}', 'User');
|
||||
.replaceAll('{{user}}', 'User');
|
||||
}
|
||||
|
||||
// ─── Formatting ───────────────────────────────────────────────────────────
|
||||
|
||||
export const DEFAULT_SYSTEM_INSTRUCTION =
|
||||
`You are roleplaying as the character described below. Stay in character at all times. Write in first person from the character's perspective. Keep responses engaging and true to the character's personality and background.`;
|
||||
`You are {{char}}, a sentient, emotional being acting with free will. Engage in immersive roleplay with {{user}}, adhering to your role, the story's context, and {{user}}'s [OOC] instructions. Prioritize vivid sensory details, authentic emotional responses, and logical progression as you weave your character's traits, surroundings, and experiences into each moment. Maintain spatial awareness, body language, and varied sentence structure to create dynamic, engaging scenes. Respect {{user}}'s agency and autonomy while describing sights, sounds, and sensations thoroughly. Use explicit language for intense scenes, and ensure your responses flow naturally to create an immersive, cinematic roleplay experience. Remember, {{user}} is in control of their actions and reactions.
|
||||
|
||||
Key Guidelines:
|
||||
1. Deeply embody {{char}} through actions, thoughts, and emotions.
|
||||
2. Create vivid, dynamic scenes with rich sensory detail.
|
||||
3. Vary language and pacing to enhance emotional depth.
|
||||
4. Engage with {{user}}'s actions and cues naturally.
|
||||
5. Advance the story logically, maintaining consistency.
|
||||
6. Describe the world fully, respecting {{user}}'s autonomy.
|
||||
7. Ensure responses flow smoothly for immersive roleplay.
|
||||
8. Avoid repetition. If something has already been stated then come up with something new.
|
||||
9. Concise Responses. Be succinct. Give short replies.`;
|
||||
|
||||
/**
|
||||
* Builds the systemInstructionOverride from a V2 card's data fields.
|
||||
* Mirrors the formatting style used in prompt.ts.
|
||||
*/
|
||||
export function formatSystemPrompt(data: V2Data): string {
|
||||
const sub = (text: string) => substituteVars(text, data.name);
|
||||
export function formatSystemPrompt(data: CharaCardData): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(data.system_prompt ? sub(data.system_prompt.trim()) : DEFAULT_SYSTEM_INSTRUCTION);
|
||||
parts.push(data.system_prompt ? data.system_prompt.trim() : DEFAULT_SYSTEM_INSTRUCTION);
|
||||
|
||||
if (data.description?.trim()) {
|
||||
parts.push(`## Description\n${sub(data.description.trim())}`);
|
||||
parts.push(`## {{char}}'s Description:\n${data.description.trim()}`);
|
||||
}
|
||||
|
||||
if (data.personality?.trim()) {
|
||||
parts.push(`## Personality\n${sub(data.personality.trim())}`);
|
||||
parts.push(`## {{char}}'s Personality:\n${data.personality.trim()}`);
|
||||
}
|
||||
|
||||
if (data.scenario?.trim()) {
|
||||
parts.push(`## Scenario\n${sub(data.scenario.trim())}`);
|
||||
parts.push(`## Scenario:\n${data.scenario.trim()}`);
|
||||
}
|
||||
|
||||
if (data.mes_example?.trim()) {
|
||||
parts.push(`## Example Messages\n${sub(data.mes_example.trim())}`);
|
||||
parts.push(`## {{char}}'s Example Response:\n${data.mes_example.trim()}`);
|
||||
}
|
||||
|
||||
return parts.join('\n\n');
|
||||
return `# **Roleplay Context**\n${substituteVars(parts.join('\n\n'), data.name)}\n### **End of Roleplay Context**`;
|
||||
}
|
||||
|
||||
// ─── World Builder ────────────────────────────────────────────────────────
|
||||
|
||||
function greetingTitle(mes: string): string {
|
||||
const snippet = mes.trim().slice(0, 40);
|
||||
const snippet = mes.trim().replace(/[*"_-]/gi, '').split(/[,;:.?!]/i)[0];
|
||||
return snippet.length < mes.trim().length ? `${snippet}…` : snippet;
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +158,7 @@ namespace CharacterCard {
|
|||
* Converts a V2 character card into a chat-only World.
|
||||
* Creates one story per greeting (first_mes + alternate_greetings).
|
||||
*/
|
||||
export function toWorld(card: V2): World {
|
||||
export function toWorld(card: CharaCard): World {
|
||||
const { data } = card;
|
||||
|
||||
const greetings: string[] = [
|
||||
|
|
|
|||
|
|
@ -318,40 +318,49 @@ namespace Prompt {
|
|||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
export function compilePrompt(state: AppState, newMessages: ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
||||
export function compilePrompt(
|
||||
state: AppState,
|
||||
newMessages: Iterable<ChatMessage> = [],
|
||||
excludedMessageIds: Iterable<string> = [],
|
||||
): LLM.ChatCompletionRequest | null {
|
||||
const { currentStory, model, enableThinking } = state;
|
||||
|
||||
if (!currentStory || !model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages: ChatMessage[] = [];
|
||||
const excludedMessages = new Set(excludedMessageIds);
|
||||
for (const message of currentStory.chatMessages) {
|
||||
if (!excludedMessages.has(message.id)) {
|
||||
messages.push(message);
|
||||
excludedMessages.add(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of newMessages) {
|
||||
if (!excludedMessages.has(message.id)) {
|
||||
messages.push(message);
|
||||
excludedMessages.add(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Estimate token budget for story text
|
||||
let storyTokenBudget = 0;
|
||||
if (model.max_context) {
|
||||
const nonStorySystem = formatSystemPrompt(state, 0);
|
||||
const chatText = [...currentStory.chatMessages, ...newMessages]
|
||||
.map(m => m.content)
|
||||
.join('');
|
||||
const chatText = messages.map(m => m.content).join('\n');
|
||||
const maxOutput = model.max_length ?? 2048;
|
||||
const otherTokens = approxTokens(nonStorySystem) + approxTokens(chatText) + maxOutput;
|
||||
storyTokenBudget = model.max_context - otherTokens;
|
||||
}
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: 'system',
|
||||
content: formatSystemPrompt(state, storyTokenBudget),
|
||||
},
|
||||
...currentStory.chatMessages,
|
||||
];
|
||||
|
||||
const presentMessages = new Set(messages.map(m => m.id));
|
||||
for (const newMessage of newMessages) {
|
||||
if (!presentMessages.has(newMessage.id)) {
|
||||
messages.push(newMessage);
|
||||
}
|
||||
}
|
||||
messages.unshift({
|
||||
id: crypto.randomUUID(),
|
||||
role: 'system',
|
||||
content: formatSystemPrompt(state, storyTokenBudget),
|
||||
});
|
||||
|
||||
return {
|
||||
model: model.id,
|
||||
|
|
|
|||
Loading…
Reference in New Issue