yuuki-deidad

This commit is contained in:
aguitauwu
2026-02-09 19:36:27 -06:00
commit efdbd95700
34 changed files with 5639 additions and 0 deletions

189
app/api/chat/route.ts Normal file
View File

@@ -0,0 +1,189 @@
import { NextRequest, NextResponse } from "next/server";
const HF_MODELS: Record<string, string> = {
"yuuki-v0.1":
"https://api-inference.huggingface.co/models/YuuKi-OS/Yuuki-v0.1",
"yuuki-3.7":
"https://api-inference.huggingface.co/models/YuuKi-OS/Yuuki-3.7",
"yuuki-best":
"https://api-inference.huggingface.co/models/YuuKi-OS/Yuuki-best",
};
const YUUKI_API_MODELS: Record<string, string> = {
"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.
* This is an OpenAI-compatible endpoint.
*/
async function callYuukiApi(
token: string,
model: string,
messages: { role: string; content: string }[]
) {
const modelId = YUUKI_API_MODELS[model] || "yuuki-best";
const response = await fetch("https://yuuki-api.vercel.app/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
model: modelId,
messages,
max_tokens: 1024,
temperature: 0.7,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Yuuki API error (${response.status}): ${errorText.slice(0, 200)}`
);
}
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 directly with an hf_ token.
*/
async function callHuggingFace(
token: string,
model: string,
messages: { role: string; content: string }[]
) {
const modelUrl = HF_MODELS[model] || HF_MODELS["yuuki-best"];
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(modelUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: prompt,
parameters: {
max_new_tokens: 1024,
temperature: 0.7,
top_p: 0.9,
repetition_penalty: 1.1,
return_full_text: false,
},
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`HuggingFace error (${response.status}): ${errorText.slice(0, 200)}`
);
}
const data = await response.json();
let generatedText = "";
if (Array.isArray(data) && data[0]?.generated_text) {
generatedText = data[0].generated_text.trim();
} else if (typeof data === "string") {
generatedText = data.trim();
} else if (data?.generated_text) {
generatedText = data.generated_text.trim();
} else {
generatedText = JSON.stringify(data);
}
// Clean up 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:
generatedText ||
"I received your message but couldn't generate a response. Please try again.",
id: `chatcmpl-${Date.now()}`,
model,
};
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { messages, model, token, tokenSource } = body;
if (!messages || !Array.isArray(messages)) {
return NextResponse.json(
{ error: "messages is required" },
{ status: 400 }
);
}
const modelKey = model || "yuuki-best";
if (!HF_MODELS[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 }
);
}
return NextResponse.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

54
app/api/research/route.ts Normal file
View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const { query } = await req.json();
if (!query) {
return NextResponse.json({ error: "query is required" }, { status: 400 });
}
const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "Tavily API key not configured" }, { status: 500 });
}
const response = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: apiKey,
query,
search_depth: "advanced",
include_answer: true,
include_raw_content: false,
max_results: 5,
}),
});
if (!response.ok) {
const text = await response.text();
return NextResponse.json(
{ error: `Tavily error: ${response.status}`, details: text },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json({
answer: data.answer || "",
results: (data.results || []).map(
(r: { title: string; url: string; content: string; score: number }) => ({
title: r.title,
url: r.url,
content: r.content,
score: r.score,
})
),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

62
app/api/youtube/route.ts Normal file
View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const { query } = await req.json();
if (!query) {
return NextResponse.json({ error: "query is required" }, { status: 400 });
}
const apiKey = process.env.YOUTUBE_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "YouTube API key not configured" }, { status: 500 });
}
const params = new URLSearchParams({
part: "snippet",
q: query,
key: apiKey,
maxResults: "5",
type: "video",
});
const response = await fetch(
`https://www.googleapis.com/youtube/v3/search?${params.toString()}`
);
if (!response.ok) {
const text = await response.text();
return NextResponse.json(
{ error: `YouTube error: ${response.status}`, details: text },
{ status: response.status }
);
}
const data = await response.json();
const videos = (data.items || []).map(
(item: {
id: { videoId: string };
snippet: {
title: string;
description: string;
channelTitle: string;
thumbnails: { medium: { url: string } };
};
}) => ({
id: item.id.videoId,
title: item.snippet.title,
description: item.snippet.description,
channel: item.snippet.channelTitle,
thumbnail: item.snippet.thumbnails?.medium?.url || "",
url: `https://youtube.com/watch?v=${item.id.videoId}`,
})
);
return NextResponse.json({ videos });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

200
app/globals.css Normal file
View File

@@ -0,0 +1,200 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
/* ── Light ── */
:root {
--background: #fafafa;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #0a0a0a;
--primary-foreground: #fafafa;
--secondary: #f0f0f0;
--secondary-foreground: #0a0a0a;
--muted: #f0f0f0;
--muted-foreground: #6b6b6b;
--accent: #e8e8e8;
--accent-foreground: #0a0a0a;
--destructive: #dc2626;
--destructive-foreground: #fafafa;
--border: #e0e0e0;
--input: #e0e0e0;
--ring: #0a0a0a;
--radius: 0.625rem;
--user-accent: #0a0a0a;
--glass: rgba(255, 255, 255, 0.72);
--glass-border: rgba(0, 0, 0, 0.08);
/* Shadcn/ui default additions */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
/* ── Dark ── */
.dark {
--background: #09090b;
--foreground: #fafafa;
--card: #111113;
--card-foreground: #fafafa;
--popover: #111113;
--popover-foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #09090b;
--secondary: #1c1c1f;
--secondary-foreground: #fafafa;
--muted: #1c1c1f;
--muted-foreground: #8b8b8b;
--accent: #252528;
--accent-foreground: #fafafa;
--destructive: #dc2626;
--destructive-foreground: #fafafa;
--border: #252528;
--input: #252528;
--ring: #fafafa;
--user-accent: #fafafa;
--glass: rgba(17, 17, 19, 0.78);
--glass-border: rgba(255, 255, 255, 0.06);
/* Shadcn/ui default additions */
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
/* ── Pastel ── */
.pastel {
--background: #fdf6f0;
--foreground: #2d2420;
--card: #fff9f5;
--card-foreground: #2d2420;
--popover: #fff9f5;
--popover-foreground: #2d2420;
--primary: #c08b6e;
--primary-foreground: #fff9f5;
--secondary: #f5ece5;
--secondary-foreground: #2d2420;
--muted: #f5ece5;
--muted-foreground: #8a7568;
--accent: #ecddd1;
--accent-foreground: #2d2420;
--destructive: #c97070;
--destructive-foreground: #fff9f5;
--border: #e8d8cc;
--input: #e8d8cc;
--ring: #c08b6e;
--user-accent: #c08b6e;
--glass: rgba(253, 246, 240, 0.78);
--glass-border: rgba(192, 139, 110, 0.12);
}
@theme inline {
--font-sans: 'Geist', 'Geist Fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Mono Fallback', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Shadcn/ui default additions */
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
/* Custom scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 3px;
opacity: 0.3;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
opacity: 0.5;
}
/* Typing animation */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.cursor-blink {
animation: blink 1s step-end infinite;
}
/* Glass morphism utility */
.glass {
background: var(--glass);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-color: var(--glass-border);
}
/* Smooth transitions for theme changes */
html {
transition: background-color 0.3s ease, color 0.3s ease;
}

71
app/layout.tsx Normal file
View File

@@ -0,0 +1,71 @@
import React from "react";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import { Toaster } from "sonner";
import { YuukiThemeProvider } from "@/lib/theme-context";
import { AuthProvider } from "@/lib/auth-context";
import "./globals.css";
const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Yuuki Chat - AI Chat Interface for Yuuki Models",
description:
"A beautiful macOS-inspired AI chat interface powered by Yuuki language models. Features research, YouTube search, and customizable themes.",
icons: {
icon: [
{
url: "/icon-light-32x32.png",
media: "(prefers-color-scheme: light)",
},
{
url: "/icon-dark-32x32.png",
media: "(prefers-color-scheme: dark)",
},
{
url: "/icon.svg",
type: "image/svg+xml",
},
],
apple: "/apple-icon.png",
},
generator: 'v0.app'
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
{ media: "(prefers-color-scheme: dark)", color: "#09090b" },
],
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<body className="font-sans antialiased">
<AuthProvider>
<YuukiThemeProvider>
{children}
<Toaster
position="top-center"
toastOptions={{
className: "bg-card text-card-foreground border-border",
}}
/>
</YuukiThemeProvider>
</AuthProvider>
<Analytics />
</body>
</html>
);
}

15
app/page.tsx Normal file
View File

@@ -0,0 +1,15 @@
"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 <TokenScreen />;
}
return <ChatWindow />;
}