feat(ai): refine chat UX and add AI roadmap view

This commit is contained in:
Brad Stein 2025-12-20 23:52:49 -03:00
parent 113cade1b2
commit 370007813d
4 changed files with 124 additions and 33 deletions

View File

@ -126,7 +126,7 @@ export function fallbackServices() {
name: "AI Chat", name: "AI Chat",
category: "ai", category: "ai",
summary: "LLM chat (public beta)", summary: "LLM chat (public beta)",
link: "/ai", link: "/ai/chat",
host: "chat.ai.bstein.dev", host: "chat.ai.bstein.dev",
status: "live", status: "live",
}, },
@ -134,7 +134,7 @@ export function fallbackServices() {
name: "AI Image", name: "AI Image",
category: "ai", category: "ai",
summary: "Visualization tool - Planned", summary: "Visualization tool - Planned",
link: "/ai", link: "/ai/roadmap",
host: "draw.ai.bstein.dev", host: "draw.ai.bstein.dev",
status: "planned", status: "planned",
}, },
@ -142,7 +142,7 @@ export function fallbackServices() {
name: "AI Speech", name: "AI Speech",
category: "ai", category: "ai",
summary: "Live Translation - Planned", summary: "Live Translation - Planned",
link: "/ai", link: "/ai/roadmap",
host: "talk.ai.bstein.dev", host: "talk.ai.bstein.dev",
status: "planned", status: "planned",
}, },

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router";
import HomeView from "./views/HomeView.vue"; import HomeView from "./views/HomeView.vue";
import AboutView from "./views/AboutView.vue"; import AboutView from "./views/AboutView.vue";
import AiView from "./views/AiView.vue"; import AiView from "./views/AiView.vue";
import AiPlanView from "./views/AiPlanView.vue";
import MoneroView from "./views/MoneroView.vue"; import MoneroView from "./views/MoneroView.vue";
export default createRouter({ export default createRouter({
@ -9,7 +10,9 @@ export default createRouter({
routes: [ routes: [
{ path: "/", name: "home", component: HomeView }, { path: "/", name: "home", component: HomeView },
{ path: "/about", name: "about", component: AboutView }, { path: "/about", name: "about", component: AboutView },
{ path: "/ai", name: "ai", component: AiView }, { path: "/ai", redirect: "/ai/chat" },
{ path: "/ai/chat", name: "ai-chat", component: AiView },
{ path: "/ai/roadmap", name: "ai-roadmap", component: AiPlanView },
{ path: "/monero", name: "monero", component: MoneroView }, { path: "/monero", name: "monero", component: MoneroView },
], ],
}); });

View File

@ -0,0 +1,89 @@
<template>
<div class="page">
<section class="card hero glass">
<div>
<p class="eyebrow">Atlas AI</p>
<h1>Roadmap</h1>
<p class="lede">
Chat is live today. Image generation and speech / translation will roll out next. This page tracks whats planned and
what hardware it will land on.
</p>
</div>
</section>
<section class="card grid">
<div class="track">
<div class="pill mono">AI Image</div>
<h3>Visualization</h3>
<p class="text">
Goal: small, fast image generation for diagrams, thumbnails, and mockups. Targeting Jetson nodes once stable. Output
will be gated to members only.
</p>
<ul>
<li>Models: open-source SD/FLUX variants distilled for 16GB GPUs.</li>
<li>Pipeline: upload prompt queued job signed URL in Nextcloud.</li>
<li>Status: planned (no UI yet).</li>
</ul>
</div>
<div class="track">
<div class="pill mono">AI Speech</div>
<h3>Voice + Translation</h3>
<p class="text">
Goal: low-latency ASR + TTS for meetings and media. Results should stream back into apps like Jitsi and Pegasus.
</p>
<ul>
<li>Models: whisper-style ASR, lightweight TTS with multilingual support.</li>
<li>Targets: titan-20/21 Jetsons for acceleration; fall back to CPU-only if needed.</li>
<li>Status: planned (no UI yet).</li>
</ul>
</div>
</section>
<section class="card">
<h2>Whats live now?</h2>
<p class="text">
Atlas AI chat is running on local GPU hardware at <code>chat.ai.bstein.dev</code>. The chat page streams responses and
reports latency per turn. As larger models come online on the Jetsons, the chat endpoint will be upgraded in-place.
</p>
<div class="pill mono">Next step: migrate chat to Jetsons when available</div>
</section>
</div>
</template>
<style scoped>
.page {
max-width: 1100px;
margin: 0 auto;
padding: 32px 22px 72px;
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.track {
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 16px;
background: rgba(255, 255, 255, 0.02);
display: flex;
flex-direction: column;
gap: 8px;
}
.text {
color: var(--text-muted);
}
ul {
margin: 0;
padding-left: 18px;
color: var(--text-muted);
}
.pill {
display: inline-block;
}
</style>

View File

@ -5,8 +5,8 @@
<p class="eyebrow">Atlas AI</p> <p class="eyebrow">Atlas AI</p>
<h1>Chat</h1> <h1>Chat</h1>
<p class="lede"> <p class="lede">
Lightweight LLM running on titan-24 (RTX 3080, 8GB). Anyone can chat without auth. The client streams responses and Lightweight LLM running on local GPU accelerated hardware. Anyone can chat without auth. The client streams responses
shows round-trip latency for each turn. and shows round-trip latency for each turn.
</p> </p>
<div class="pill mono pill-live">Online</div> <div class="pill mono pill-live">Online</div>
</div> </div>
@ -17,7 +17,7 @@
</div> </div>
<div class="fact"> <div class="fact">
<span class="label mono">GPU</span> <span class="label mono">GPU</span>
<span class="value mono">titan-24 · 3080 (8GB)</span> <span class="value mono">local GPU (dynamic)</span>
</div> </div>
<div class="fact"> <div class="fact">
<span class="label mono">Endpoint</span> <span class="label mono">Endpoint</span>
@ -30,7 +30,7 @@
<div class="chat-window" ref="chatWindow"> <div class="chat-window" ref="chatWindow">
<div v-for="(msg, idx) in messages" :key="idx" :class="['chat-row', msg.role]"> <div v-for="(msg, idx) in messages" :key="idx" :class="['chat-row', msg.role]">
<div class="bubble" :class="{ streaming: msg.streaming }"> <div class="bubble" :class="{ streaming: msg.streaming }">
<div class="role mono">{{ msg.role === 'assistant' ? 'ai' : 'you' }}</div> <div class="role mono">{{ msg.role === 'assistant' ? 'Atlas AI' : 'you' }}</div>
<p>{{ msg.content }}</p> <p>{{ msg.content }}</p>
<div v-if="msg.streaming" class="meta mono typing">streaming</div> <div v-if="msg.streaming" class="meta mono typing">streaming</div>
<div v-else-if="msg.latency_ms" class="meta mono">{{ msg.latency_ms }} ms</div> <div v-else-if="msg.latency_ms" class="meta mono">{{ msg.latency_ms }} ms</div>
@ -48,25 +48,17 @@
v-model="draft" v-model="draft"
placeholder="Ask anything about the lab or general topics..." placeholder="Ask anything about the lab or general topics..."
rows="3" rows="3"
@keydown="handleKeydown"
:disabled="sending" :disabled="sending"
/> />
<div class="actions"> <div class="actions">
<span class="hint mono">Shift+Enter for newline</span> <span class="hint mono">Enter to send · Shift+Enter for newline</span>
<button class="primary" type="submit" :disabled="sending || !draft.trim()"> <button class="primary" type="submit" :disabled="sending || !draft.trim()">
{{ sending ? "Sending..." : "Send" }} {{ sending ? "Sending..." : "Send" }}
</button> </button>
</div> </div>
</form> </form>
</section> </section>
<section class="card info-card">
<h2>Notes</h2>
<ul>
<li>Backend proxies requests to Ollama inside the cluster; no external calls are made.</li>
<li>Short-term context: the chat history in this page is sent each turn. Refresh clears it.</li>
<li>Future: swap in larger models on the Jetsons, add streaming and rate limits.</li>
</ul>
</section>
</div> </div>
</template> </template>
@ -80,7 +72,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const messages = ref([ const messages = ref([
{ {
role: "assistant", role: "assistant",
content: "Hi! I'm the Titan Lab assistant running on titan-24. How can I help?", content: "Hi! I'm Atlas AI. How can I help?",
}, },
]); ]);
const draft = ref(""); const draft = ref("");
@ -162,6 +154,13 @@ async function typeReveal(entry, text) {
} }
entry.streaming = false; entry.streaming = false;
} }
function handleKeydown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
</script> </script>
<style scoped> <style scoped>