From 71ef3bc46f91cbddfda58ef153e6a10738b86ae2 Mon Sep 17 00:00:00 2001 From: v0 Date: Wed, 11 Feb 2026 23:05:39 +0000 Subject: [PATCH] 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> --- app/api/chat/route.ts | 148 ++++++++++-------------------- app/page.tsx | 8 -- components/chat-window.tsx | 20 +--- components/token-screen.tsx | 178 ------------------------------------ lib/auth-context.tsx | 81 ++-------------- 5 files changed, 56 insertions(+), 379 deletions(-) delete mode 100644 components/token-screen.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 1cf9930..959c58b 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,39 +1,37 @@ import { NextRequest, NextResponse } from "next/server"; -const HF_MODELS: Record = { - "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_URL = "https://opceanai-yuuki-api.hf.space/generate"; -const YUUKI_API_MODELS: Record = { - "yuuki-v0.1": "yuuki-v0.1", - "yuuki-3.7": "yuuki-3.7", - "yuuki-best": "yuuki-best", -}; +const VALID_MODELS = ["yuuki-best", "yuuki-3.7", "yuuki-v0.1"]; /** - * Calls the Yuuki API (yuuki-api.vercel.app) with a yk- token. - * This is an OpenAI-compatible endpoint. + * Calls the Yuuki API hosted on HuggingFace Spaces. + * Open platform — no token required. */ -async function callYuukiApi( - token: string, - model: string, - messages: { role: string; content: string }[] +async function callYuukiAPI( + messages: { role: string; content: string }[], + model: 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", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify({ - model: modelId, - messages, - max_tokens: 1024, + prompt, + max_new_tokens: 1024, temperature: 0.7, + model, }), }); @@ -45,52 +43,36 @@ async function callYuukiApi( } const data = await response.json(); - const content = - data.choices?.[0]?.message?.content || data.content || "No response"; - return { content, id: data.id || `chatcmpl-${Date.now()}`, model: modelId }; -} -/** - * Calls HuggingFace Inference API via the new router.huggingface.co endpoint. - * Uses the OpenAI-compatible chat completions format. - */ -async function callHuggingFace( - 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`; + // Handle various response formats from the HF Space + let generatedText = ""; - 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)}` - ); + 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); } - const data = await response.json(); - const content = - data.choices?.[0]?.message?.content?.trim() || "No response generated."; + // Clean up conversational artifacts + const cutoffs = ["User:", "System:", "\nUser", "\nSystem"]; + for (const cutoff of cutoffs) { + const idx = generatedText.indexOf(cutoff); + if (idx > 0) generatedText = generatedText.substring(0, idx).trim(); + } return { - content, - id: data.id || `chatcmpl-${Date.now()}`, + content: + generatedText || + "I received your message but couldn't generate a response. Please try again.", + id: `chatcmpl-${Date.now()}`, model, }; } @@ -98,7 +80,7 @@ async function callHuggingFace( export async function POST(req: NextRequest) { try { const body = await req.json(); - const { messages, model, token, tokenSource } = body; + const { messages, model } = body; if (!messages || !Array.isArray(messages)) { return NextResponse.json( @@ -108,47 +90,11 @@ export async function POST(req: NextRequest) { } const modelKey = model || "yuuki-best"; - if (!HF_MODELS[modelKey]) { + if (!VALID_MODELS.includes(modelKey)) { return NextResponse.json({ error: "Invalid model" }, { status: 400 }); } - let result; - - 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 } - ); - } - + const result = await callYuukiAPI(messages, modelKey); return NextResponse.json(result); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/app/page.tsx b/app/page.tsx index 244a3f7..21c4b13 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,15 +1,7 @@ "use client"; -import { useAuth } from "@/lib/auth-context"; -import { TokenScreen } from "@/components/token-screen"; import { ChatWindow } from "@/components/chat-window"; export default function Home() { - const { isAuthenticated } = useAuth(); - - if (!isAuthenticated) { - return ; - } - return ; } diff --git a/components/chat-window.tsx b/components/chat-window.tsx index 37bf7ef..7414234 100644 --- a/components/chat-window.tsx +++ b/components/chat-window.tsx @@ -1,7 +1,6 @@ "use client"; import React, { useState, useRef, useEffect, useCallback } from "react"; -import { useAuth } from "@/lib/auth-context"; import { useYuukiTheme } from "@/lib/theme-context"; import { ChatMessage, type ChatMsg } from "./chat-message"; import { ModelSelector } from "./model-selector"; @@ -9,7 +8,6 @@ import { ThemePanel } from "./theme-panel"; import { Send, Palette, - LogOut, Globe, Youtube, Loader2, @@ -28,7 +26,6 @@ interface Conversation { } export function ChatWindow() { - const { token, tokenSource, logout } = useAuth(); const { accentColor } = useYuukiTheme(); const [conversations, setConversations] = useState([ @@ -177,8 +174,6 @@ export function ChatWindow() { body: JSON.stringify({ messages: newMessages.map((m) => ({ role: m.role, content: m.content })), model, - token: tokenSource === "demo" ? null : token, - tokenSource, }), }); @@ -316,14 +311,7 @@ export function ChatWindow() { Theme - + @@ -341,11 +329,7 @@ export function ChatWindow() { - {tokenSource === "demo" - ? "demo" - : tokenSource === "yuuki-api" - ? "yk-api" - : "hf"} + open diff --git a/components/token-screen.tsx b/components/token-screen.tsx deleted file mode 100644 index 7223fc1..0000000 --- a/components/token-screen.tsx +++ /dev/null @@ -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(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 ( -
- {/* Background pattern */} -
-
-
-
- -
- {step === "choose" ? ( - -
- {/* Logo and title */} -
-
- Y -
-
-

- Welcome to Yuuki Chat -

-

- Choose how to authenticate to start chatting -

-
-
- - {/* Two big buttons */} -
- - - -
- - {/* Small demo button */} - -
-
- ) : ( - { setStep("choose"); setError(""); setTokenValue(""); }} - > -
-
-
- {selectedSource === "yuuki-api" ? ( - - ) : ( - - )} -
-
-

- {selectedSource === "yuuki-api" ? "Enter Yuuki API Token" : "Enter Hugging Face Token"} -

-

- {selectedSource === "yuuki-api" - ? "Get your token from yuuki-api.vercel.app" - : "Get your token from huggingface.co/settings/tokens"} -

-
-
- -
-
- { 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 - /> - -
- {error &&

{error}

} -
- -
- - -
-
-
- )} -
-
- ); -} diff --git a/lib/auth-context.tsx b/lib/auth-context.tsx index b3a5ff2..2d63715 100644 --- a/lib/auth-context.tsx +++ b/lib/auth-context.tsx @@ -1,89 +1,22 @@ "use client"; -import React, { - createContext, - useContext, - useState, - useCallback, - useEffect, -} from "react"; - -export type TokenSource = "yuuki-api" | "huggingface" | "demo"; +import React, { createContext, useContext } from "react"; interface AuthContextValue { - token: string | null; - tokenSource: TokenSource | null; - setAuth: (token: string, source: TokenSource) => void; logout: () => void; - isAuthenticated: boolean; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { - const [token, setToken] = useState(null); - const [tokenSource, setTokenSource] = useState(null); - const [mounted, setMounted] = useState(false); - - 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 ( - - {null} - - ); - } + const logout = () => { + // No-op since there's no authentication needed. + // Kept for compatibility with components that reference it. + window.location.reload(); + }; return ( - + {children} );