Mark Jitsi live and refresh AI chat
This commit is contained in:
parent
33e6082b56
commit
db4dcb5059
@ -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.
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user