rename frontend subfolder

This commit is contained in:
Sebarocks 2025-07-31 16:48:55 -04:00
parent 0a894237bb
commit 16f084bfcd
21 changed files with 12 additions and 2 deletions

View file

@ -0,0 +1,66 @@
<script>
import ChatMessage from "./ChatMessage.svelte";
import { chatStore } from "./chatStore.svelte.js";
</script>
<!-- header -->
<header class="p-4">
<h1 class="text-5xl font-bold">Multi AI Chat</h1>
</header>
<!-- messages -->
<main class="flex-1 p-4 space-y-3 overflow-y-auto">
{#each chatStore.messages as m (m.id)}
<ChatMessage message={m} />
{/each}
{#if chatStore.loading}
<div class="chat chat-start">
<div class="chat-bubble chat-bubble-secondary loading"></div>
</div>
{/if}
</main>
<!-- input -->
<footer class="bg-neutral-content rounded-xl">
<div class="flex items-center">
<div class="form-control flex-1 m-4">
<textarea
class="textarea w-full"
rows="1"
placeholder="Type something…"
bind:value={chatStore.input}
onkeydown={chatStore.handleKey}
disabled={chatStore.loading}
></textarea>
</div>
</div>
<div class="flex items-center m-4">
<select
class="select select-bordered join-item"
bind:value={chatStore.model}
disabled={chatStore.loading || chatStore.loadingModels}
>
{#if chatStore.loadingModels}
<option value="" disabled>Loading models...</option>
{:else if chatStore.models.length === 0}
<option value="" disabled>No model available</option>
{:else}
{#each chatStore.models as modelOption}
<option value={modelOption.id || modelOption}>
{modelOption.name || modelOption.id || modelOption}
</option>
{/each}
{/if}
</select>
<button
class="btn btn-primary ml-auto"
onclick={chatStore.send}
disabled={!chatStore.input.trim() || chatStore.models.length === 0}
>
{#if chatStore.loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}Send{/if}
</button>
</div>
</footer>

View file

@ -0,0 +1,33 @@
<script>
import { chatStore } from "./chatStore.svelte.js";
</script>
<aside class="menu p-4 w-64 bg-base-200 min-h-full">
<div class="flex justify-between items-center mb-4">
<span class="text-lg font-bold">Chats</span>
<button
class="btn btn-xs btn-primary"
onclick={() =>
chatStore.selectChat(null) && chatStore.createAndSelect()}
>
New
</button>
</div>
<ul class="menu menu-compact">
{#each chatStore.history as c}
<li>
<a
href="/{c.id}"
class={chatStore.chatId === c.id ? "active" : ""}
onclick={(e) => {
e.preventDefault();
chatStore.selectChat(c.id);
}}
>
{c.title}
</a>
</li>
{/each}
</ul>
</aside>

View file

@ -0,0 +1,29 @@
<script>
import { marked } from "marked";
let { message } = $props(); // { id, role, text }
const text = $derived(message.text);
const me = $derived(message.role == "user");
/* optional: allow HTML inside the markdown (default is escaped) */
marked.setOptions({ breaks: true, gfm: true });
</script>
{#if me}
<div class="chat chat-end">
<div class="chat-bele chat-bubble chat-bubble-primary">
{text}
</div>
</div>
{:else}
<div class="chat chat-start">
<div
class="chat-bele chat-bubble {message.role === 'error'
? 'text-error'
: ''} prose max-w-none"
>
<!-- eslint-disable svelte/no-at-html-tags -->
{@html marked(text)}
</div>
</div>
{/if}

View file

@ -0,0 +1,35 @@
const API = "http://localhost:8000"; // change if needed
export async function createChat(model = "qwen/qwen3-235b-a22b-2507") {
const r = await fetch(`${API}/chats`, {
method: "POST",
body: JSON.stringify({ model }),
});
return r.json(); // { chat_id }
}
export async function sendUserMessage(chatId, text, model = "") {
const r = await fetch(`${API}/chats/${chatId}/messages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text, model }),
});
return r.json(); // { message_id }
}
export function openStream(chatId, messageId) {
return new EventSource(
`${API}/chats/${chatId}/stream?message_id=${messageId}`,
);
}
export async function fetchModels() {
try {
const response = await fetch(`${API}/models`);
const data = await response.json();
return data.models || [];
} catch (error) {
console.error('Failed to fetch models:', error);
return [];
}
}

View file

@ -0,0 +1,153 @@
import { createChat, sendUserMessage, openStream, fetchModels } from "./chatApi.svelte.js";
const STORAGE_KEY = "chatHistory";
function loadHistory() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
} catch {
return [];
}
}
function saveHistory(list) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
export const chatStore = (() => {
let chatId = $state(null);
let messages = $state([]);
let loading = $state(false);
let input = $state("");
let model = $state("qwen/qwen3-235b-a22b-2507"); // default
let models = $state([]);
let loadingModels = $state(true);
// public helpers
const history = $derived(loadHistory());
function pushHistory(id, title, msgs) {
console.log(`push history: ${id} - ${title}`);
const h = history.filter((c) => c.id !== id);
h.unshift({ id, title, messages: msgs });
saveHistory(h.slice(0, 50)); // keep last 50
}
async function selectChat(id) {
if (id === chatId) return;
chatId = id;
const stored = loadHistory().find((c) => c.id === id);
messages = stored?.messages || [];
loading = true;
loading = false;
window.history.replaceState({}, "", `/${id}`);
}
async function createAndSelect() {
const { id } = await createChat(model);
console.log(id);
selectChat(id);
return id;
}
async function send() {
if (!input.trim()) return;
if (!chatId) await createAndSelect();
const userMsg = { id: crypto.randomUUID(), role: "user", text: input };
messages = [...messages, userMsg];
pushHistory(chatId, userMsg.text.slice(0, 30), messages);
loading = true;
const { message_id } = await sendUserMessage(chatId, input, model);
input = "";
let assistantMsg = { id: message_id, role: "assistant", text: "" };
messages = [...messages, assistantMsg];
const es = openStream(chatId, message_id);
es.onmessage = (e) => {
assistantMsg = { ...assistantMsg, text: assistantMsg.text + e.data };
messages = [...messages.slice(0, -1), assistantMsg];
};
es.onerror = () => {
es.close();
loading = false;
};
es.addEventListener("done", (e) => {
console.log(e);
es.close();
loading = false;
pushHistory(chatId, userMsg.text.slice(0, 30), messages);
});
}
async function loadModels() {
loadingModels = true;
models = await fetchModels();
loadingModels = false;
// Set default model if available and not already set
if (models.length > 0 && !model) {
model = models[0].id || models[0];
}
}
function handleKey(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}
// initial route handling
const path = window.location.pathname.slice(1);
const storedHistory = loadHistory();
if (path && !storedHistory.find((c) => c.id === path)) {
createAndSelect();
} else if (path) {
selectChat(path);
}
// Load models on initialization
loadModels();
return {
get chatId() {
return chatId;
},
get messages() {
return messages;
},
get loading() {
return loading;
},
get input() {
return input;
},
set input(v) {
input = v;
},
get model() {
return model;
},
set model(v) {
model = v;
},
get models() {
return models;
},
get loadingModels() {
return loadingModels;
},
get history() {
return loadHistory();
},
selectChat,
send,
handleKey,
createAndSelect,
loadModels,
};
})();

View file

@ -0,0 +1,31 @@
import { chatStore } from "./chatStore.svelte.js";
// keyed by chat_id → chatStore instance
const cache = $state({});
// which chat is on screen right now
export const activeChatId = $state(null);
export function getStore(chatId) {
if (!cache[chatId]) {
cache[chatId] = chatStore(chatId);
}
return cache[chatId];
}
export function switchChat(chatId) {
activeChatId = chatId;
}
export function newChat() {
const id = "chat_" + crypto.randomUUID();
switchChat(id);
return id;
}
// restore last opened chat (or create first one)
(() => {
const ids = JSON.parse(localStorage.getItem("chat_ids") || "[]");
if (ids.length) switchChat(ids[0]);
else newChat();
})();