mirror of
https://github.com/YuuKi-OS/Yuuki-chat.git
synced 2026-02-18 22:01:09 +00:00
feat: migrate to open HuggingFace API and remove auth
Remove auth logic and switch to open API endpoint. Co-authored-by: awa <212803252+aguitauwu@users.noreply.github.com>
This commit is contained in:
@@ -1,39 +1,37 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
const HF_MODELS: Record<string, string> = {
|
const YUUKI_API_URL = "https://opceanai-yuuki-api.hf.space/generate";
|
||||||
"yuuki-v0.1": "YuuKi-OS/Yuuki-v0.1",
|
|
||||||
"yuuki-3.7": "YuuKi-OS/Yuuki-3.7",
|
|
||||||
"yuuki-best": "YuuKi-OS/Yuuki-best",
|
|
||||||
};
|
|
||||||
|
|
||||||
const YUUKI_API_MODELS: Record<string, string> = {
|
const VALID_MODELS = ["yuuki-best", "yuuki-3.7", "yuuki-v0.1"];
|
||||||
"yuuki-v0.1": "yuuki-v0.1",
|
|
||||||
"yuuki-3.7": "yuuki-3.7",
|
|
||||||
"yuuki-best": "yuuki-best",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the Yuuki API (yuuki-api.vercel.app) with a yk- token.
|
* Calls the Yuuki API hosted on HuggingFace Spaces.
|
||||||
* This is an OpenAI-compatible endpoint.
|
* Open platform — no token required.
|
||||||
*/
|
*/
|
||||||
async function callYuukiApi(
|
async function callYuukiAPI(
|
||||||
token: string,
|
messages: { role: string; content: string }[],
|
||||||
model: string,
|
model: string
|
||||||
messages: { role: string; content: string }[]
|
|
||||||
) {
|
) {
|
||||||
const modelId = YUUKI_API_MODELS[model] || "yuuki-best";
|
// Build a prompt from the message history
|
||||||
|
const prompt = messages
|
||||||
|
.map((m) => {
|
||||||
|
if (m.role === "system") return `System: ${m.content}`;
|
||||||
|
if (m.role === "user") return `User: ${m.content}`;
|
||||||
|
if (m.role === "assistant") return `Assistant: ${m.content}`;
|
||||||
|
return m.content;
|
||||||
|
})
|
||||||
|
.join("\n") + "\nAssistant:";
|
||||||
|
|
||||||
const response = await fetch("https://yuuki-api.vercel.app/api/chat", {
|
const response = await fetch(YUUKI_API_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: modelId,
|
prompt,
|
||||||
messages,
|
max_new_tokens: 1024,
|
||||||
max_tokens: 1024,
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
|
model,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,52 +43,36 @@ async function callYuukiApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const content =
|
|
||||||
data.choices?.[0]?.message?.content || data.content || "No response";
|
// Handle various response formats from the HF Space
|
||||||
return { content, id: data.id || `chatcmpl-${Date.now()}`, model: modelId };
|
let generatedText = "";
|
||||||
|
|
||||||
|
if (typeof data === "string") {
|
||||||
|
generatedText = data.trim();
|
||||||
|
} else if (data?.generated_text) {
|
||||||
|
generatedText = data.generated_text.trim();
|
||||||
|
} else if (data?.response) {
|
||||||
|
generatedText = data.response.trim();
|
||||||
|
} else if (data?.output) {
|
||||||
|
generatedText = data.output.trim();
|
||||||
|
} else if (Array.isArray(data) && data[0]?.generated_text) {
|
||||||
|
generatedText = data[0].generated_text.trim();
|
||||||
|
} else {
|
||||||
|
generatedText = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Clean up conversational artifacts
|
||||||
* Calls HuggingFace Inference API via the new router.huggingface.co endpoint.
|
const cutoffs = ["User:", "System:", "\nUser", "\nSystem"];
|
||||||
* Uses the OpenAI-compatible chat completions format.
|
for (const cutoff of cutoffs) {
|
||||||
*/
|
const idx = generatedText.indexOf(cutoff);
|
||||||
async function callHuggingFace(
|
if (idx > 0) generatedText = generatedText.substring(0, idx).trim();
|
||||||
token: string,
|
|
||||||
model: string,
|
|
||||||
messages: { role: string; content: string }[]
|
|
||||||
) {
|
|
||||||
const modelId = HF_MODELS[model] || HF_MODELS["yuuki-best"];
|
|
||||||
const url = `https://router.huggingface.co/hf-inference/models/${modelId}/v1/chat/completions`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: modelId,
|
|
||||||
messages,
|
|
||||||
max_tokens: 1024,
|
|
||||||
temperature: 0.7,
|
|
||||||
top_p: 0.9,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`HuggingFace error (${response.status}): ${errorText.slice(0, 200)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const content =
|
|
||||||
data.choices?.[0]?.message?.content?.trim() || "No response generated.";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content:
|
||||||
id: data.id || `chatcmpl-${Date.now()}`,
|
generatedText ||
|
||||||
|
"I received your message but couldn't generate a response. Please try again.",
|
||||||
|
id: `chatcmpl-${Date.now()}`,
|
||||||
model,
|
model,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -98,7 +80,7 @@ async function callHuggingFace(
|
|||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { messages, model, token, tokenSource } = body;
|
const { messages, model } = body;
|
||||||
|
|
||||||
if (!messages || !Array.isArray(messages)) {
|
if (!messages || !Array.isArray(messages)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -108,47 +90,11 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modelKey = model || "yuuki-best";
|
const modelKey = model || "yuuki-best";
|
||||||
if (!HF_MODELS[modelKey]) {
|
if (!VALID_MODELS.includes(modelKey)) {
|
||||||
return NextResponse.json({ error: "Invalid model" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid model" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
const result = await callYuukiAPI(messages, modelKey);
|
||||||
|
|
||||||
if (tokenSource === "demo") {
|
|
||||||
// Demo mode: use server-side HF_DEMO_TOKEN directly against HuggingFace
|
|
||||||
const demoToken = process.env.HF_DEMO_TOKEN;
|
|
||||||
if (!demoToken) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Demo token not configured on server" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result = await callHuggingFace(demoToken, modelKey, messages);
|
|
||||||
} else if (tokenSource === "yuuki-api") {
|
|
||||||
// Yuuki API: yk- tokens go to yuuki-api.vercel.app
|
|
||||||
if (!token) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "No API token provided" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result = await callYuukiApi(token, modelKey, messages);
|
|
||||||
} else if (tokenSource === "huggingface") {
|
|
||||||
// HuggingFace: hf_ tokens go directly to HF Inference API
|
|
||||||
if (!token) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "No API token provided" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result = await callHuggingFace(token, modelKey, messages);
|
|
||||||
} else {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid token source" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAuth } from "@/lib/auth-context";
|
|
||||||
import { TokenScreen } from "@/components/token-screen";
|
|
||||||
import { ChatWindow } from "@/components/chat-window";
|
import { ChatWindow } from "@/components/chat-window";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { isAuthenticated } = useAuth();
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return <TokenScreen />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ChatWindow />;
|
return <ChatWindow />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
|
||||||
import { useYuukiTheme } from "@/lib/theme-context";
|
import { useYuukiTheme } from "@/lib/theme-context";
|
||||||
import { ChatMessage, type ChatMsg } from "./chat-message";
|
import { ChatMessage, type ChatMsg } from "./chat-message";
|
||||||
import { ModelSelector } from "./model-selector";
|
import { ModelSelector } from "./model-selector";
|
||||||
@@ -9,7 +8,6 @@ import { ThemePanel } from "./theme-panel";
|
|||||||
import {
|
import {
|
||||||
Send,
|
Send,
|
||||||
Palette,
|
Palette,
|
||||||
LogOut,
|
|
||||||
Globe,
|
Globe,
|
||||||
Youtube,
|
Youtube,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -28,7 +26,6 @@ interface Conversation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatWindow() {
|
export function ChatWindow() {
|
||||||
const { token, tokenSource, logout } = useAuth();
|
|
||||||
const { accentColor } = useYuukiTheme();
|
const { accentColor } = useYuukiTheme();
|
||||||
|
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([
|
const [conversations, setConversations] = useState<Conversation[]>([
|
||||||
@@ -177,8 +174,6 @@ export function ChatWindow() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: newMessages.map((m) => ({ role: m.role, content: m.content })),
|
messages: newMessages.map((m) => ({ role: m.role, content: m.content })),
|
||||||
model,
|
model,
|
||||||
token: tokenSource === "demo" ? null : token,
|
|
||||||
tokenSource,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,14 +311,7 @@ export function ChatWindow() {
|
|||||||
<Palette className="h-4 w-4" />
|
<Palette className="h-4 w-4" />
|
||||||
Theme
|
Theme
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={logout}
|
|
||||||
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
|
||||||
>
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -341,11 +329,7 @@ export function ChatWindow() {
|
|||||||
</button>
|
</button>
|
||||||
<ModelSelector value={model} onChange={setModel} />
|
<ModelSelector value={model} onChange={setModel} />
|
||||||
<span className="hidden sm:inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-[10px] font-mono text-muted-foreground uppercase tracking-wider">
|
<span className="hidden sm:inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-[10px] font-mono text-muted-foreground uppercase tracking-wider">
|
||||||
{tokenSource === "demo"
|
open
|
||||||
? "demo"
|
|
||||||
: tokenSource === "yuuki-api"
|
|
||||||
? "yk-api"
|
|
||||||
: "hf"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { MacOSWindow } from "./macos-window";
|
|
||||||
import { useAuth, type TokenSource } from "@/lib/auth-context";
|
|
||||||
import { Key, ExternalLink, Sparkles, Eye, EyeOff, ArrowRight } from "lucide-react";
|
|
||||||
|
|
||||||
export function TokenScreen() {
|
|
||||||
const { setAuth } = useAuth();
|
|
||||||
const [step, setStep] = useState<"choose" | "input">("choose");
|
|
||||||
const [selectedSource, setSelectedSource] = useState<TokenSource | null>(null);
|
|
||||||
const [tokenValue, setTokenValue] = useState("");
|
|
||||||
const [showToken, setShowToken] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSourceSelect = (source: TokenSource) => {
|
|
||||||
if (source === "demo") {
|
|
||||||
setLoading(true);
|
|
||||||
// Demo mode: token is managed server-side via HF_DEMO_TOKEN env var
|
|
||||||
setAuth("__demo__", "demo");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedSource(source);
|
|
||||||
setStep("input");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!tokenValue.trim()) {
|
|
||||||
setError("Please enter your API token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedSource) return;
|
|
||||||
setAuth(tokenValue.trim(), selectedSource);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center p-4 bg-background">
|
|
||||||
{/* Background pattern */}
|
|
||||||
<div className="pointer-events-none fixed inset-0 overflow-hidden">
|
|
||||||
<div className="absolute -top-40 -right-40 h-80 w-80 rounded-full bg-muted/30 blur-3xl" />
|
|
||||||
<div className="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-muted/20 blur-3xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative w-full max-w-lg">
|
|
||||||
{step === "choose" ? (
|
|
||||||
<MacOSWindow title="yuuki-chat ~ authenticate" className="animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
||||||
<div className="flex flex-col items-center gap-8">
|
|
||||||
{/* Logo and title */}
|
|
||||||
<div className="flex flex-col items-center gap-3 pt-2">
|
|
||||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-foreground">
|
|
||||||
<span className="text-2xl font-bold text-background font-mono">Y</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-foreground text-balance">
|
|
||||||
Welcome to Yuuki Chat
|
|
||||||
</h1>
|
|
||||||
<p className="mt-1.5 text-sm text-muted-foreground leading-relaxed">
|
|
||||||
Choose how to authenticate to start chatting
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Two big buttons */}
|
|
||||||
<div className="grid w-full grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSourceSelect("yuuki-api")}
|
|
||||||
className="group flex flex-col items-center gap-3 rounded-xl border border-border bg-card p-6 transition-all hover:border-foreground/20 hover:bg-muted/50 hover:shadow-lg"
|
|
||||||
>
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-foreground text-background transition-transform group-hover:scale-105">
|
|
||||||
<Key className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-sm font-semibold text-foreground">Yuuki API</div>
|
|
||||||
<div className="mt-0.5 text-xs text-muted-foreground">yuuki-api.vercel.app</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSourceSelect("huggingface")}
|
|
||||||
className="group flex flex-col items-center gap-3 rounded-xl border border-border bg-card p-6 transition-all hover:border-foreground/20 hover:bg-muted/50 hover:shadow-lg"
|
|
||||||
>
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-foreground text-background transition-transform group-hover:scale-105">
|
|
||||||
<ExternalLink className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-sm font-semibold text-foreground">Hugging Face</div>
|
|
||||||
<div className="mt-0.5 text-xs text-muted-foreground">huggingface.co token</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Small demo button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSourceSelect("demo")}
|
|
||||||
disabled={loading}
|
|
||||||
className="flex items-center gap-2 rounded-lg px-4 py-2 text-xs text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/50 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Sparkles className="h-3.5 w-3.5" />
|
|
||||||
{loading ? "Connecting..." : "Use demo"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</MacOSWindow>
|
|
||||||
) : (
|
|
||||||
<MacOSWindow
|
|
||||||
title={`yuuki-chat ~ ${selectedSource === "yuuki-api" ? "yuuki-api token" : "hugging face token"}`}
|
|
||||||
className="animate-in fade-in slide-in-from-bottom-4 duration-500"
|
|
||||||
onClose={() => { setStep("choose"); setError(""); setTokenValue(""); }}
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col items-center gap-3 pt-2">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-foreground text-background">
|
|
||||||
{selectedSource === "yuuki-api" ? (
|
|
||||||
<Key className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<ExternalLink className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
|
||||||
{selectedSource === "yuuki-api" ? "Enter Yuuki API Token" : "Enter Hugging Face Token"}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground leading-relaxed">
|
|
||||||
{selectedSource === "yuuki-api"
|
|
||||||
? "Get your token from yuuki-api.vercel.app"
|
|
||||||
: "Get your token from huggingface.co/settings/tokens"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type={showToken ? "text" : "password"}
|
|
||||||
value={tokenValue}
|
|
||||||
onChange={(e) => { setTokenValue(e.target.value); setError(""); }}
|
|
||||||
placeholder={selectedSource === "yuuki-api" ? "yk-xxxxxxxxxx" : "hf_xxxxxxxxxx"}
|
|
||||||
className="w-full rounded-lg border border-border bg-background px-4 py-3 pr-10 text-sm text-foreground font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-ring/20 focus:border-foreground/30"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowToken(!showToken)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setStep("choose"); setError(""); setTokenValue(""); }}
|
|
||||||
className="flex-1 rounded-lg border border-border py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-foreground py-2.5 text-sm font-medium text-background transition-opacity hover:opacity-90"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</MacOSWindow>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
import React, { createContext, useContext } from "react";
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
export type TokenSource = "yuuki-api" | "huggingface" | "demo";
|
|
||||||
|
|
||||||
interface AuthContextValue {
|
interface AuthContextValue {
|
||||||
token: string | null;
|
|
||||||
tokenSource: TokenSource | null;
|
|
||||||
setAuth: (token: string, source: TokenSource) => void;
|
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
isAuthenticated: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [token, setToken] = useState<string | null>(null);
|
const logout = () => {
|
||||||
const [tokenSource, setTokenSource] = useState<TokenSource | null>(null);
|
// No-op since there's no authentication needed.
|
||||||
const [mounted, setMounted] = useState(false);
|
// Kept for compatibility with components that reference it.
|
||||||
|
window.location.reload();
|
||||||
useEffect(() => {
|
};
|
||||||
const stored = localStorage.getItem("yuuki-token");
|
|
||||||
const storedSource = localStorage.getItem(
|
|
||||||
"yuuki-token-source"
|
|
||||||
) as TokenSource | null;
|
|
||||||
if (stored && storedSource) {
|
|
||||||
setToken(stored);
|
|
||||||
setTokenSource(storedSource);
|
|
||||||
}
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setAuth = useCallback((t: string, source: TokenSource) => {
|
|
||||||
setToken(t);
|
|
||||||
setTokenSource(source);
|
|
||||||
if (source === "demo") {
|
|
||||||
// Don't persist demo sessions for security
|
|
||||||
localStorage.setItem("yuuki-token", "__demo__");
|
|
||||||
localStorage.setItem("yuuki-token-source", "demo");
|
|
||||||
} else {
|
|
||||||
localStorage.setItem("yuuki-token", t);
|
|
||||||
localStorage.setItem("yuuki-token-source", source);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
setToken(null);
|
|
||||||
setTokenSource(null);
|
|
||||||
localStorage.removeItem("yuuki-token");
|
|
||||||
localStorage.removeItem("yuuki-token-source");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Prevent flash of token screen before localStorage is read
|
|
||||||
if (!mounted) {
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider
|
|
||||||
value={{
|
|
||||||
token: null,
|
|
||||||
tokenSource: null,
|
|
||||||
setAuth,
|
|
||||||
logout,
|
|
||||||
isAuthenticated: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{null}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider value={{ logout }}>
|
||||||
value={{
|
|
||||||
token,
|
|
||||||
tokenSource,
|
|
||||||
setAuth,
|
|
||||||
logout,
|
|
||||||
isAuthenticated: !!token,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user