Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
+450
View File
@@ -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.");