Mark Jitsi live and refresh AI chat

This commit is contained in:
Brad Stein 2025-12-22 16:52:25 -03:00
parent 33e6082b56
commit db4dcb5059
4 changed files with 71 additions and 61 deletions

View File

@ -36,7 +36,7 @@ AI_CHAT_SYSTEM_PROMPT = os.getenv(
AI_CHAT_TIMEOUT_SEC = float(os.getenv("AI_CHAT_TIMEOUT_SEC", "20")) AI_CHAT_TIMEOUT_SEC = float(os.getenv("AI_CHAT_TIMEOUT_SEC", "20"))
AI_NODE_NAME = os.getenv("AI_CHAT_NODE_NAME") or os.getenv("AI_NODE_NAME") or "ai-cluster" AI_NODE_NAME = os.getenv("AI_CHAT_NODE_NAME") or os.getenv("AI_NODE_NAME") or "ai-cluster"
AI_GPU_DESC = os.getenv("AI_CHAT_GPU_DESC") or "local GPU (dynamic)" AI_GPU_DESC = os.getenv("AI_CHAT_GPU_DESC") or "local GPU (dynamic)"
AI_PUBLIC_ENDPOINT = os.getenv("AI_PUBLIC_CHAT_ENDPOINT", "https://chat.ai.bstein.dev/api/ai/chat") AI_PUBLIC_ENDPOINT = os.getenv("AI_PUBLIC_CHAT_ENDPOINT", "https://chat.ai.bstein.dev/api/chat")
AI_K8S_LABEL = os.getenv("AI_K8S_LABEL", "app=ollama") AI_K8S_LABEL = os.getenv("AI_K8S_LABEL", "app=ollama")
AI_K8S_NAMESPACE = os.getenv("AI_K8S_NAMESPACE", "ai") AI_K8S_NAMESPACE = os.getenv("AI_K8S_NAMESPACE", "ai")
AI_MODEL_ANNOTATION = os.getenv("AI_MODEL_ANNOTATION", "ai.bstein.dev/model") AI_MODEL_ANNOTATION = os.getenv("AI_MODEL_ANNOTATION", "ai.bstein.dev/model")
@ -154,6 +154,7 @@ def lab_status() -> Any:
return jsonify(payload) return jsonify(payload)
@app.route("/api/chat", methods=["POST"])
@app.route("/api/ai/chat", methods=["POST"]) @app.route("/api/ai/chat", methods=["POST"])
def ai_chat() -> Any: def ai_chat() -> Any:
payload = request.get_json(silent=True) or {} payload = request.get_json(silent=True) or {}
@ -190,6 +191,7 @@ def ai_chat() -> Any:
return jsonify({"error": str(exc)}), 502 return jsonify({"error": str(exc)}), 502
@app.route("/api/chat/info", methods=["GET"])
@app.route("/api/ai/info", methods=["GET"]) @app.route("/api/ai/info", methods=["GET"])
def ai_info() -> Any: def ai_info() -> Any:
meta = _discover_ai_meta() meta = _discover_ai_meta()
@ -205,7 +207,7 @@ def _discover_ai_meta() -> dict[str, str]:
"node": AI_NODE_NAME, "node": AI_NODE_NAME,
"gpu": AI_GPU_DESC, "gpu": AI_GPU_DESC,
"model": AI_CHAT_MODEL, "model": AI_CHAT_MODEL,
"endpoint": AI_PUBLIC_ENDPOINT or "/api/ai/chat", "endpoint": AI_PUBLIC_ENDPOINT or "/api/chat",
} }
# Only attempt k8s if we're in-cluster and credentials exist. # Only attempt k8s if we're in-cluster and credentials exist.

View File

@ -51,6 +51,8 @@ const goAbout = () => router.push("/about");
.avatar { .avatar {
width: 44px; width: 44px;
height: 44px; height: 44px;
aspect-ratio: 1 / 1;
flex-shrink: 0;
border-radius: 50%; border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.14);
overflow: hidden; overflow: hidden;
@ -63,6 +65,8 @@ const goAbout = () => router.push("/about");
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block;
border-radius: 50%;
} }
.name { .name {

View File

@ -118,34 +118,34 @@ export function fallbackServices() {
{ {
name: "Jitsi", name: "Jitsi",
category: "conferencing", category: "conferencing",
summary: "Video Conferencing - Planned", summary: "Video conferencing",
link: "https://meet.bstein.dev", link: "https://meet.bstein.dev",
status: "degraded", status: "live",
},
{
name: "AI Chat",
category: "ai",
summary: "LLM chat (public beta)",
link: "/ai/chat",
host: "chat.ai.bstein.dev",
status: "live",
},
{
name: "AI Image",
category: "ai",
summary: "Visualization tool - Planned",
link: "/ai/roadmap",
host: "draw.ai.bstein.dev",
status: "planned",
},
{
name: "AI Speech",
category: "ai",
summary: "Live Translation - Planned",
link: "/ai/roadmap",
host: "talk.ai.bstein.dev",
status: "planned",
}, },
{
name: "AI Chat",
category: "ai",
summary: "LLM chat (public beta)",
link: "/ai/chat",
host: "chat.ai.bstein.dev",
status: "live",
},
{
name: "AI Image",
category: "ai",
summary: "Visualization tool - Planned",
link: "/ai/roadmap",
host: "draw.ai.bstein.dev",
status: "planned",
},
{
name: "AI Speech",
category: "ai",
summary: "Live Translation - Planned",
link: "/ai/roadmap",
host: "talk.ai.bstein.dev",
status: "planned",
},
], ],
}; };
} }

View File

@ -5,8 +5,7 @@
<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 local GPU accelerated hardware. Anyone can chat without auth. The client streams responses, Talk with Atlas AI. It knows a surprising amount about technology!
shows round-trip latency for each turn, and we're training an Atlas-aware model steeped in Titan Lab context.
</p> </p>
<div class="pill mono pill-live">Online</div> <div class="pill mono pill-live">Online</div>
</div> </div>
@ -22,7 +21,7 @@
<div class="fact"> <div class="fact">
<span class="label mono">Endpoint</span> <span class="label mono">Endpoint</span>
<button class="endpoint-copy mono" type="button" @click="copyCurl"> <button class="endpoint-copy mono" type="button" @click="copyCurl">
{{ meta.endpoint || apiHost }} {{ meta.endpoint || apiDisplay }}
<span v-if="copied" class="copied">copied</span> <span v-if="copied" class="copied">copied</span>
</button> </button>
</div> </div>
@ -62,39 +61,22 @@
</div> </div>
</form> </form>
</section> </section>
<section class="card">
<h2>API quick start</h2>
<p class="text">
The UI hits <code>/api/ai/chat</code> on the same host. External callers can hit the public endpoint
<code>https://chat.ai.bstein.dev/api/ai/chat</code>. POST JSON with a <code>message</code> and optional
<code>history</code>.
</p>
<pre class="mono snippet">
curl -X POST https://chat.ai.bstein.dev/api/ai/chat \
-H 'content-type: application/json' \
-d '{"message":"hi, what is your name?"}'</pre
>
<p class="text">
Responses stream in the UI. API callers get either a JSON body <code>{ reply, latency_ms }</code> or a streamed text
body if available.
</p>
</section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, onUpdated, ref } from "vue"; import { onMounted, onUpdated, ref } from "vue";
const API_URL = (import.meta.env.VITE_AI_ENDPOINT || "/api/ai/chat").trim(); const API_URL = (import.meta.env.VITE_AI_ENDPOINT || "/api/chat").trim();
const apiHost = new URL(API_URL, window.location.href).host + new URL(API_URL, window.location.href).pathname; const apiUrl = new URL(API_URL, window.location.href);
const apiDisplay = apiUrl.host + apiUrl.pathname;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const meta = ref({ const meta = ref({
model: "loading...", model: "loading...",
gpu: "local GPU (dynamic)", gpu: "local GPU (dynamic)",
node: "unknown", node: "unknown",
endpoint: "", endpoint: apiUrl.toString(),
}); });
const messages = ref([ const messages = ref([
{ {
@ -125,7 +107,7 @@ async function fetchMeta() {
model: data.model || meta.value.model, model: data.model || meta.value.model,
gpu: data.gpu || meta.value.gpu, gpu: data.gpu || meta.value.gpu,
node: data.node || meta.value.node, node: data.node || meta.value.node,
endpoint: data.endpoint || meta.value.endpoint || apiHost, endpoint: data.endpoint || meta.value.endpoint || apiDisplay,
}; };
} catch { } catch {
// swallow // swallow
@ -209,7 +191,7 @@ function handleKeydown(e) {
} }
async function copyCurl() { async function copyCurl() {
const target = meta.value.endpoint || new URL(API_URL, window.location.href).toString(); const target = meta.value.endpoint || apiUrl.toString();
const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '{\"message\":\"hi\"}'`; const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '{\"message\":\"hi\"}'`;
try { try {
await navigator.clipboard.writeText(curl); await navigator.clipboard.writeText(curl);
@ -223,7 +205,7 @@ async function copyCurl() {
<style scoped> <style scoped>
.page { .page {
max-width: 1100px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 32px 22px 72px; padding: 32px 22px 72px;
} }
@ -232,6 +214,7 @@ async function copyCurl() {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
gap: 18px; gap: 18px;
align-items: start;
} }
.hero-facts { .hero-facts {
@ -280,6 +263,10 @@ async function copyCurl() {
.chat-card { .chat-card {
margin-top: 18px; margin-top: 18px;
display: grid;
grid-template-rows: 1fr auto;
gap: 12px;
min-height: 60vh;
} }
.chat-window { .chat-window {
@ -287,8 +274,8 @@ async function copyCurl() {
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
border-radius: 12px; border-radius: 12px;
padding: 14px; padding: 14px;
min-height: 320px; min-height: 360px;
max-height: 520px; height: 100%;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -344,7 +331,7 @@ async function copyCurl() {
} }
.chat-form { .chat-form {
margin-top: 12px; margin-top: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
@ -386,8 +373,25 @@ button:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.info-card ul { @media (max-width: 820px) {
color: var(--text-muted); .hero {
padding-left: 18px; grid-template-columns: 1fr;
}
.chat-card {
min-height: 50vh;
}
.actions {
align-items: flex-start;
flex-direction: column;
}
}
@media (min-width: 1100px) {
.chat-card {
min-height: calc(100vh - 260px);
}
.chat-window {
min-height: 480px;
}
} }
</style> </style>