Add Games File
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
const STORAGE_KEY = "funnygirl.identityPromptLab.v2";
|
||||
|
||||
const runtimeDefaults = {
|
||||
model: "deepseek-chat",
|
||||
temperature: 0.7,
|
||||
maxTokens: 700,
|
||||
apiBase: "https://api.deepseek.com",
|
||||
apiKey: "",
|
||||
mockMode: true,
|
||||
};
|
||||
|
||||
const defaultPresets = [
|
||||
{
|
||||
id: "blank",
|
||||
title: "Blank",
|
||||
fields: [""],
|
||||
...runtimeDefaults,
|
||||
},
|
||||
{
|
||||
id: "kira-test",
|
||||
title: "Kira test",
|
||||
fields: [
|
||||
"Identity:\n- Name: Кира\n- Age: 22",
|
||||
"Ты не называешь себя ассистентом и не объясняешь, что являешься языковой моделью. Ты отвечаешь как живой собеседник внутри текущей сцены.",
|
||||
"Ты говоришь естественно, короткими живыми фразами. Если эмоция важнее факта, сначала отвечаешь на эмоцию.",
|
||||
"Respond only with a valid JSON object. Match this response shape exactly:\n{\n \"reply\": \"Text response goes here.\"\n}",
|
||||
],
|
||||
...runtimeDefaults,
|
||||
},
|
||||
];
|
||||
|
||||
const el = {
|
||||
presetSelect: document.querySelector("#presetSelect"),
|
||||
savePresetButton: document.querySelector("#savePresetButton"),
|
||||
newPresetButton: document.querySelector("#newPresetButton"),
|
||||
themeToggle: document.querySelector("#themeToggle"),
|
||||
tabs: [...document.querySelectorAll(".tab")],
|
||||
panels: [...document.querySelectorAll(".tab-panel")],
|
||||
promptFields: document.querySelector("#promptFields"),
|
||||
addFieldButton: document.querySelector("#addFieldButton"),
|
||||
clearFieldsButton: document.querySelector("#clearFieldsButton"),
|
||||
chatTitle: document.querySelector("#chatTitle"),
|
||||
promptPreview: document.querySelector("#promptPreview"),
|
||||
promptStats: document.querySelector("#promptStats"),
|
||||
messages: document.querySelector("#messages"),
|
||||
chatForm: document.querySelector("#chatForm"),
|
||||
messageInput: document.querySelector("#messageInput"),
|
||||
sendButton: document.querySelector("#sendButton"),
|
||||
clearChatButton: document.querySelector("#clearChatButton"),
|
||||
copyPromptButton: document.querySelector("#copyPromptButton"),
|
||||
temperatureValue: document.querySelector("#temperatureValue"),
|
||||
};
|
||||
|
||||
const runtimeFields = {
|
||||
model: document.querySelector("#modelInput"),
|
||||
temperature: document.querySelector("#temperatureInput"),
|
||||
maxTokens: document.querySelector("#maxTokensInput"),
|
||||
apiBase: document.querySelector("#apiBaseInput"),
|
||||
apiKey: document.querySelector("#apiKeyInput"),
|
||||
mockMode: document.querySelector("#mockModeInput"),
|
||||
};
|
||||
|
||||
let state = loadState();
|
||||
let selectedId = state.selectedId || state.presets[0].id;
|
||||
let chat = [];
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||
if (saved?.presets?.length) {
|
||||
return normalizeState(saved);
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
return {
|
||||
presets: structuredClone(defaultPresets),
|
||||
selectedId: defaultPresets[0].id,
|
||||
theme: "light",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeState(nextState) {
|
||||
return {
|
||||
theme: nextState.theme || "light",
|
||||
selectedId: nextState.selectedId,
|
||||
presets: nextState.presets.map((preset) => ({
|
||||
id: preset.id,
|
||||
title: preset.title || "Untitled",
|
||||
fields: Array.isArray(preset.fields) && preset.fields.length ? preset.fields : [""],
|
||||
model: preset.model || runtimeDefaults.model,
|
||||
temperature: Number.isFinite(Number(preset.temperature)) ? Number(preset.temperature) : runtimeDefaults.temperature,
|
||||
maxTokens: Number.isFinite(Number(preset.maxTokens)) ? Number(preset.maxTokens) : runtimeDefaults.maxTokens,
|
||||
apiBase: normalizeApiBase(preset.apiBase),
|
||||
apiKey: preset.apiKey || "",
|
||||
mockMode: preset.mockMode ?? runtimeDefaults.mockMode,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApiBase(apiBase) {
|
||||
const cleanBase = String(apiBase || runtimeDefaults.apiBase).replace(/\/+$/, "");
|
||||
return cleanBase === "https://api.deepseek.com/v1" ? runtimeDefaults.apiBase : cleanBase;
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
state.selectedId = selectedId;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
function currentPreset() {
|
||||
return state.presets.find((preset) => preset.id === selectedId) || state.presets[0];
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
state.theme = theme;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
saveState();
|
||||
}
|
||||
|
||||
function renderPresetSelect() {
|
||||
el.presetSelect.innerHTML = "";
|
||||
state.presets.forEach((preset) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = preset.id;
|
||||
option.textContent = preset.title;
|
||||
el.presetSelect.append(option);
|
||||
});
|
||||
el.presetSelect.value = selectedId;
|
||||
}
|
||||
|
||||
function loadPresetToForm(preset) {
|
||||
Object.entries(runtimeFields).forEach(([key, input]) => {
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = Boolean(preset[key]);
|
||||
} else {
|
||||
input.value = preset[key] ?? "";
|
||||
}
|
||||
});
|
||||
|
||||
renderPromptFields(preset.fields);
|
||||
el.chatTitle.textContent = preset.title;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function renderPromptFields(fields) {
|
||||
el.promptFields.innerHTML = "";
|
||||
fields.forEach((value, index) => {
|
||||
el.promptFields.append(createPromptField(value, index));
|
||||
});
|
||||
}
|
||||
|
||||
function createPromptField(value, index) {
|
||||
const wrapper = document.createElement("article");
|
||||
wrapper.className = "prompt-field";
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.rows = 6;
|
||||
textarea.value = value || "";
|
||||
textarea.placeholder = "Заполни блок system prompt...";
|
||||
textarea.dataset.index = String(index);
|
||||
textarea.addEventListener("input", () => {
|
||||
persistFormToPreset();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "field-controls";
|
||||
controls.append(
|
||||
makeFieldButton("↑", "Move up", () => moveField(index, -1)),
|
||||
makeFieldButton("↓", "Move down", () => moveField(index, 1)),
|
||||
makeFieldButton("×", "Remove", () => removeField(index)),
|
||||
);
|
||||
|
||||
wrapper.append(textarea, controls);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function makeFieldButton(text, title, onClick) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = text;
|
||||
button.title = title;
|
||||
button.addEventListener("click", onClick);
|
||||
return button;
|
||||
}
|
||||
|
||||
function readRuntimeForm() {
|
||||
const next = {};
|
||||
Object.entries(runtimeFields).forEach(([key, input]) => {
|
||||
if (input.type === "checkbox") {
|
||||
next[key] = input.checked;
|
||||
} else if (input.type === "number" || input.type === "range") {
|
||||
next[key] = Number(input.value);
|
||||
} else {
|
||||
next[key] = input.value;
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function readPromptFields() {
|
||||
const values = [...el.promptFields.querySelectorAll("textarea")].map((textarea) => textarea.value);
|
||||
return values.length ? values : [""];
|
||||
}
|
||||
|
||||
function persistFormToPreset() {
|
||||
const index = state.presets.findIndex((preset) => preset.id === selectedId);
|
||||
if (index === -1) return;
|
||||
state.presets[index] = {
|
||||
...state.presets[index],
|
||||
...readRuntimeForm(),
|
||||
fields: readPromptFields(),
|
||||
};
|
||||
saveState();
|
||||
}
|
||||
|
||||
function buildPrompt() {
|
||||
return readPromptFields()
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const prompt = buildPrompt();
|
||||
el.promptPreview.textContent = prompt;
|
||||
el.promptStats.textContent = `${prompt.length} chars`;
|
||||
el.temperatureValue.textContent = Number(runtimeFields.temperature.value || 0).toFixed(2);
|
||||
}
|
||||
|
||||
function addPromptField(value = "") {
|
||||
persistFormToPreset();
|
||||
const preset = currentPreset();
|
||||
preset.fields = [...readPromptFields(), value];
|
||||
renderPromptFields(preset.fields);
|
||||
saveState();
|
||||
updatePreview();
|
||||
el.promptFields.querySelector("textarea:last-child")?.focus();
|
||||
}
|
||||
|
||||
function clearPromptFields() {
|
||||
currentPreset().fields = [""];
|
||||
renderPromptFields(currentPreset().fields);
|
||||
saveState();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function removeField(index) {
|
||||
const values = readPromptFields();
|
||||
values.splice(index, 1);
|
||||
currentPreset().fields = values.length ? values : [""];
|
||||
renderPromptFields(currentPreset().fields);
|
||||
saveState();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function moveField(index, direction) {
|
||||
const values = readPromptFields();
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= values.length) return;
|
||||
[values[index], values[target]] = [values[target], values[index]];
|
||||
currentPreset().fields = values;
|
||||
renderPromptFields(values);
|
||||
saveState();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function addMessage(role, content) {
|
||||
chat.push({ role, content });
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${role}`;
|
||||
node.textContent = content;
|
||||
el.messages.append(node);
|
||||
el.messages.scrollTop = el.messages.scrollHeight;
|
||||
}
|
||||
|
||||
function showThinking() {
|
||||
const node = document.createElement("div");
|
||||
node.className = "message assistant thinking";
|
||||
node.id = "thinkingMessage";
|
||||
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "spinner";
|
||||
spinner.setAttribute("aria-hidden", "true");
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = "LLM думает";
|
||||
|
||||
node.append(spinner, text);
|
||||
el.messages.append(node);
|
||||
el.messages.scrollTop = el.messages.scrollHeight;
|
||||
}
|
||||
|
||||
function hideThinking() {
|
||||
document.querySelector("#thinkingMessage")?.remove();
|
||||
}
|
||||
|
||||
function mockReply(message) {
|
||||
const prompt = buildPrompt().toLowerCase();
|
||||
const short = prompt.includes("корот") || prompt.includes("short");
|
||||
const prefix = short ? "Слышу." : "Слышу тебя.";
|
||||
const tail = message.endsWith("?")
|
||||
? "Я бы уточнила сначала, что именно для тебя здесь важнее."
|
||||
: "Я отвечаю по текущим свободным полям, без API вызова.";
|
||||
return `${prefix} ${tail}`;
|
||||
}
|
||||
|
||||
function parseAssistantContent(content) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.reply || content;
|
||||
} catch {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToModel(message) {
|
||||
if (runtimeFields.mockMode.checked) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||
return mockReply(message);
|
||||
}
|
||||
|
||||
const body = {
|
||||
apiBase: runtimeFields.apiBase.value.trim(),
|
||||
apiKey: runtimeFields.apiKey.value.trim(),
|
||||
model: runtimeFields.model.value.trim(),
|
||||
temperature: Number(runtimeFields.temperature.value),
|
||||
max_tokens: Number(runtimeFields.maxTokens.value),
|
||||
messages: [
|
||||
{ role: "system", content: buildPrompt() },
|
||||
...chat.map((item) => ({ role: item.role === "system" ? "assistant" : item.role, content: item.content })),
|
||||
],
|
||||
};
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Local server is not reachable. Restart it with: node server.mjs");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "Request failed");
|
||||
}
|
||||
return data.content;
|
||||
}
|
||||
|
||||
el.tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const target = tab.dataset.tab;
|
||||
el.tabs.forEach((item) => item.classList.toggle("is-active", item === tab));
|
||||
el.panels.forEach((panel) => panel.classList.toggle("is-active", panel.dataset.panel === target));
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(runtimeFields).forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
persistFormToPreset();
|
||||
updatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
el.addFieldButton.addEventListener("click", () => addPromptField());
|
||||
el.clearFieldsButton.addEventListener("click", clearPromptFields);
|
||||
|
||||
el.presetSelect.addEventListener("change", () => {
|
||||
persistFormToPreset();
|
||||
selectedId = el.presetSelect.value;
|
||||
chat = [];
|
||||
el.messages.innerHTML = "";
|
||||
loadPresetToForm(currentPreset());
|
||||
saveState();
|
||||
});
|
||||
|
||||
el.savePresetButton.addEventListener("click", () => {
|
||||
persistFormToPreset();
|
||||
addMessage("system", `Preset "${currentPreset().title}" saved locally.`);
|
||||
});
|
||||
|
||||
el.newPresetButton.addEventListener("click", () => {
|
||||
persistFormToPreset();
|
||||
const title = prompt("Preset name", "New preset");
|
||||
if (!title) return;
|
||||
const id = `${title.toLowerCase().replace(/[^a-zа-я0-9]+/gi, "-")}-${Date.now()}`;
|
||||
state.presets.push({ ...currentPreset(), ...readRuntimeForm(), id, title, fields: [""] });
|
||||
selectedId = id;
|
||||
renderPresetSelect();
|
||||
loadPresetToForm(currentPreset());
|
||||
saveState();
|
||||
});
|
||||
|
||||
el.clearChatButton.addEventListener("click", () => {
|
||||
chat = [];
|
||||
el.messages.innerHTML = "";
|
||||
});
|
||||
|
||||
el.copyPromptButton.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(buildPrompt());
|
||||
addMessage("system", "System prompt copied.");
|
||||
});
|
||||
|
||||
el.themeToggle.addEventListener("click", () => {
|
||||
setTheme(state.theme === "dark" ? "light" : "dark");
|
||||
});
|
||||
|
||||
el.chatForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const message = el.messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
persistFormToPreset();
|
||||
addMessage("user", message);
|
||||
el.messageInput.value = "";
|
||||
el.sendButton.disabled = true;
|
||||
el.sendButton.textContent = "Wait";
|
||||
showThinking();
|
||||
|
||||
try {
|
||||
const content = await sendToModel(message);
|
||||
hideThinking();
|
||||
addMessage("assistant", parseAssistantContent(content));
|
||||
} catch (error) {
|
||||
hideThinking();
|
||||
addMessage("system", error.message);
|
||||
} finally {
|
||||
el.sendButton.disabled = false;
|
||||
el.sendButton.textContent = "Send";
|
||||
}
|
||||
});
|
||||
|
||||
el.messageInput.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
el.chatForm.requestSubmit();
|
||||
});
|
||||
|
||||
setTheme(state.theme || "light");
|
||||
renderPresetSelect();
|
||||
loadPresetToForm(currentPreset());
|
||||
addMessage("system", "Mock mode is on. Add API key and disable Mock mode for real DeepSeek calls.");
|
||||
Reference in New Issue
Block a user