Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 78 additions & 55 deletions components/auth/user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CircleUser,
CreditCard,
Gift,
LogIn,
LogOut,
MessageCircleQuestion,
Monitor,
Expand All @@ -31,7 +32,7 @@ import {
const FOOTER_LINKS = [
{ href: "/settings/preferences", label: "Profile", Icon: CircleUser },
{ href: "/settings", label: "Account Settings", Icon: SettingsIcon },
{ href: "/settings/billing", label: "Pricing", Icon: CreditCard },
{ href: "/settings/billing", label: "Pricing", Icon: CreditCard, external: true },
{
href: "https://github.com/OpenSIN-Code/SIN-Code-WebUI-v2#readme",
label: "Documentation",
Expand All @@ -58,35 +59,19 @@ export function UserMenu() {
const { data: session, isPending } = useSession()
const { theme, setTheme } = useTheme()

if (isPending) {
return (
<div className="flex items-center gap-2 px-1 opacity-50">
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-bold text-muted-foreground">
</span>
</div>
)
}

if (!session?.user) {
return (
<button
type="button"
onClick={() => router.push("/login")}
className="flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1.5 py-1 text-[12.5px] text-sidebar-foreground hover:bg-sidebar-accent"
>
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-brand text-[9px] font-bold text-white">
?
</span>
<span className="truncate font-medium">Anmelden</span>
</button>
)
}

const user = session.user as { name?: string | null; email?: string | null }
const displayName = user.name || user.email || "User"
const initial = (user.name || user.email || "U").charAt(0).toUpperCase()
const email = user.email || ""
// Show trigger even while pending / unauthenticated — the v0-style dropdown
// is always present at the sidebar footer.
const user = session?.user as
| { name?: string | null; email?: string | null }
| undefined
const isLoggedIn = Boolean(user?.name || user?.email)
const displayName = isLoggedIn
? user!.name || user!.email || "User"
: "Anmelden"
const initial = isLoggedIn
? (user!.name || user!.email || "U").charAt(0).toUpperCase()
: "?"
const email = user?.email || ""

async function handleSignOut() {
const { signOut } = await import("@/lib/auth/client")
Expand All @@ -95,32 +80,50 @@ export function UserMenu() {
router.refresh()
}

function handleSignIn() {
router.push("/login")
}

return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
type="button"
aria-label="User menu"
className="flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1.5 py-1 text-[12.5px] text-sidebar-foreground hover:bg-sidebar-accent"
aria-label={isLoggedIn ? "User menu" : "Sign in"}
className={cn(
"flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1.5 py-1 text-[12.5px] text-sidebar-foreground hover:bg-sidebar-accent",
isPending && "opacity-60",
)}
/>
}
>
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-brand text-[9px] font-bold text-white">
{initial}
{isPending ? "…" : initial}
</span>
<span className="truncate font-medium">{displayName}</span>
</DropdownMenuTrigger>

<DropdownMenuContent align="start" side="top" className="w-60">
<div className="flex flex-col gap-0.5 px-3 py-2">
<span className="truncate text-[13px] font-semibold text-foreground">
{displayName}
</span>
{email ? (
<span className="truncate text-[12px] text-muted-foreground">{email}</span>
) : null}
</div>
{isLoggedIn ? (
<div className="flex flex-col gap-0.5 px-3 py-2">
<span className="truncate text-[13px] font-semibold text-foreground">
{displayName}
</span>
{email ? (
<span className="truncate text-[12px] text-muted-foreground">{email}</span>
) : null}
</div>
) : (
<div className="flex flex-col gap-0.5 px-3 py-2">
<span className="truncate text-[13px] font-semibold text-foreground">
Nicht angemeldet
</span>
<span className="truncate text-[12px] text-muted-foreground">
Melde dich an, um Chats zu speichern
</span>
</div>
)}
<DropdownMenuSeparator />

<DropdownMenuGroup>
Expand All @@ -129,7 +132,12 @@ export function UserMenu() {
key={label}
render={
external ? (
<a href={href} target="_blank" rel="noreferrer" className="flex items-center gap-2">
<a
href={href}
target="_blank"
rel="noreferrer"
className="flex items-center gap-2"
>
<Icon className="size-4" />
{label}
</a>
Expand Down Expand Up @@ -194,7 +202,7 @@ export function UserMenu() {
</Link>
</div>

{/* Chat Position — stub (single layout) */}
{/* Chat Position — stub */}
<div className="flex items-center justify-between px-3 py-1.5">
<span className="text-[13px] text-foreground">Chat Position</span>
<Link
Expand All @@ -208,18 +216,33 @@ export function UserMenu() {
<DropdownMenuSeparator />

<DropdownMenuGroup>
<DropdownMenuItem
render={
<button
type="button"
onClick={handleSignOut}
className="flex w-full items-center gap-2"
>
<LogOut className="size-4" />
Sign Out
</button>
}
/>
{isLoggedIn ? (
<DropdownMenuItem
render={
<button
type="button"
onClick={handleSignOut}
className="flex w-full items-center gap-2"
>
<LogOut className="size-4" />
Sign Out
</button>
}
/>
) : (
<DropdownMenuItem
render={
<button
type="button"
onClick={handleSignIn}
className="flex w-full items-center gap-2"
>
<LogIn className="size-4" />
Anmelden
</button>
}
/>
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
75 changes: 69 additions & 6 deletions components/chat/chat-view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client"

import { useRef, useEffect } from "react"
import { useRef, useEffect, useState } from "react"
import { Check, Copy, SquareTerminal } from "lucide-react"
import { Message } from "@/components/chat/message"
import { ThinkingIndicator, LoadingDots } from "@/components/chat/thinking-indicator"
import { ToolCall } from "@/components/chat/tool-call"
import { PromptComposer } from "@/components/chat/prompt-composer"
import { MarkdownMessage } from "@/components/chat/markdown-message"
import { ChatHeader } from "@/components/chat/chat-header"
import { DashedSpinner, Starburst } from "@/components/icons"
import { cn } from "@/lib/utils"

export interface ChatPart {
type: "text" | "tool"
Expand All @@ -32,6 +35,51 @@ interface ChatViewProps {
title?: string
}

/* v0-style code block with copy button (preserved from initial commit) */
function CopyCodeBlock({ body, lang = "code" }: { body: string; lang?: string }) {
const [copied, setCopied] = useState(false)
async function handleCopy() {
try {
await navigator.clipboard.writeText(body)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
/* clipboard not available */
}
}
return (
<div className="overflow-hidden rounded-xl border border-border/60 bg-card">
<div className="flex items-center justify-between border-b border-border/50 px-3 py-2">
<div className="flex items-center gap-2">
<SquareTerminal className="size-3.5 text-muted-foreground" />
<span className="text-[11px] text-muted-foreground">{lang}</span>
</div>
<button
type="button"
aria-label={copied ? "Copied" : "Copy code"}
onClick={handleCopy}
className="flex size-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
>
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
</button>
</div>
<pre className="overflow-x-auto p-4 font-mono text-[12.5px] leading-[1.6]">
<code>{body}</code>
</pre>
</div>
)
}

/* v0-style tool badge (preserved from initial commit) */
function ToolBadge({ name }: { name: string }) {
return (
<div className="flex items-center gap-2 rounded-lg border border-border/60 bg-card px-3 py-2">
<DashedSpinner className="size-3.5 animate-[spin_2s_linear_infinite] text-muted-foreground" />
<span className="font-mono text-[12px] text-muted-foreground">{name}</span>
</div>
)
}

export function ChatView({
messages,
status,
Expand All @@ -48,26 +96,41 @@ export function ChatView({

const isStreaming = status === "streaming"
const lastMessage = messages[messages.length - 1]
const hasMessages = messages.length > 0

return (
<div className="flex h-dvh flex-col bg-background">
<ChatHeader title={title} />
<div className="flex-1 overflow-y-auto">
<div className="mx-auto flex max-w-3xl flex-col px-4 py-6">
{messages.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center gap-2 py-32">
<h1 className="text-2xl font-semibold text-balance">
<div className={cn("mx-auto flex flex-col px-4 py-6", hasMessages ? "max-w-3xl" : "w-full")}>
{!hasMessages && (
<div className="flex flex-1 flex-col items-center justify-center gap-4 py-16">
<Starburst className="size-10 text-brand" />
<h1 className="text-balance text-2xl font-semibold">
What do you want to create?
</h1>
<p className="text-sm text-muted-foreground">
<p className="max-w-md text-balance text-center text-sm text-muted-foreground">
Ask the SIN-Code agent to build, fix, or explain anything.
Press <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[11px]">Enter</kbd> to send.
</p>
</div>
)}

{messages.map((msg) => {
const streamingThis =
isStreaming && msg.id === lastMessage?.id && msg.role === "assistant"
if (msg.role === "user") {
return (
<div key={msg.id} className="flex justify-end">
<div className="max-w-[85%] rounded-[18px] bg-secondary px-4 py-2.5 text-[14px] leading-relaxed text-foreground">
{msg.parts
.filter((p) => p.type === "text")
.map((p) => p.text)
.join("")}
</div>
</div>
)
}
return (
<Message
key={msg.id}
Expand Down
Loading