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
9 changes: 5 additions & 4 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useState, useRef, Suspense } from "react";
import {
usePrivy,
useLogin,
useLoginWithEmail,
useLoginWithOAuth,
} from "@privy-io/react-auth";
Expand All @@ -26,7 +27,8 @@ const DiscordIcon = ({ className }: { className?: string }) => (
);

function LoginPageContent() {
const { ready, authenticated, login, user } = usePrivy();
const { ready, authenticated, user } = usePrivy();
const { login } = useLogin();
const { sendCode, loginWithCode, state: emailState } = useLoginWithEmail();
const { initOAuth } = useLoginWithOAuth();
const router = useRouter();
Expand Down Expand Up @@ -168,10 +170,9 @@ function LoginPageContent() {
lastLoginAttemptRef.current = now;
setLoadingButton("wallet");

// Use login() instead of connectWallet() for authentication
// This opens the Privy modal (non-blocking, returns immediately)
// Use login() with loginMethods restricted to 'wallet' to directly show wallet options
// Authentication state changes are handled via the authenticated state in useEffect
login();
login({ loginMethods: ["wallet"] });

// Reset the guard after a short delay to allow modal to open
// If authentication succeeds, the useEffect will handle redirect
Expand Down
2 changes: 1 addition & 1 deletion components/character-builder/character-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function CharacterForm({ character, onChange }: CharacterFormProps) {
htmlFor="username"
className="text-xs font-medium text-white/70"
>
Username
Username *
</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 select-none pointer-events-none">
Expand Down
5 changes: 5 additions & 0 deletions components/chat/character-build-mode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ export function CharacterBuildMode({
return;
}

if (!character.username) {
toast.error("Username is required");
return;
}

Comment on lines +171 to +175
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontend now requires username to be non-empty before saving (line 171-174), but the database schema and backend service allow username to be null/undefined. This creates an inconsistency - while new characters created through the UI will have usernames, the backend doesn't enforce this constraint. Consider adding backend validation in the character creation/update service to ensure username is always provided, or update the database schema to make username NOT NULL if it's truly required.

Suggested change
if (!character.username) {
toast.error("Username is required");
return;
}

Copilot uses AI. Check for mistakes.
if (!character.bio) {
toast.error("Character bio is required");
return;
Expand Down
272 changes: 221 additions & 51 deletions components/chat/eliza-chat-interface.tsx

Large diffs are not rendered by default.

37 changes: 7 additions & 30 deletions components/layout/chat-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,22 @@

import Link from "next/link";
import Image from "next/image";
import { useRouter, usePathname } from "next/navigation";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usePathname import is no longer used after removing the pathname-based logic for avatar selection. This import should be removed to keep the code clean.

Suggested change
import { useRouter } from "next/navigation";

Copilot uses AI. Check for mistakes.
import { useState, useEffect, useMemo, useRef } from "react";
import {
ArrowLeft,
X,
MessageSquare,
Loader2,
Trash2,
Copy,
Check,
Plus,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { LockOnButton } from "@/components/brand";
import { useChatStore } from "@/lib/stores/chat-store";
import { SidebarBottomPanel } from "./sidebar-bottom-panel";
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ElizaAvatar import is no longer used after removing the avatar display from the sidebar. This import should be removed.

Copilot uses AI. Check for mistakes.
import { ElizaAvatar } from "@/components/chat/eliza-avatar";

// Default Eliza avatars - different for build vs chat pages
const DEFAULT_ELIZA_AVATAR_CHAT =
"https://raw.githubusercontent.com/elizaOS/eliza-avatars/refs/heads/master/Eliza/portrait.png";
const DEFAULT_ELIZA_AVATAR_BUILD = "/avatars/eliza-default.png";

interface ChatSidebarProps {
className?: string;
Expand Down Expand Up @@ -65,14 +61,10 @@ export function ChatSidebar({
onToggle,
}: ChatSidebarProps) {
const router = useRouter();
const pathname = usePathname();
const [isMobile, setIsMobile] = useState(false);

// Use different default avatar for build vs chat pages
const isBuildPage = pathname.includes("/build");
const defaultElizaAvatar = isBuildPage
? DEFAULT_ELIZA_AVATAR_BUILD
: DEFAULT_ELIZA_AVATAR_CHAT;
const [copiedAddress, setCopiedAddress] = useState<string | null>(null);
const [showTokens, setShowTokens] = useState(false);
const tokensRef = useRef<HTMLDivElement>(null);
const {
rooms,
roomId,
Expand Down Expand Up @@ -252,21 +244,6 @@ export function ChatSidebar({
{/* Selected Character Profile with New Chat Icon */}
<div className="border-b border-white/10 px-4 py-3">
<div className="flex items-center gap-2.5">
{/* Character Avatar or Create New Agent Icon */}
{selectedCharacter ? (
<ElizaAvatar
avatarUrl={selectedCharacter.avatarUrl}
name={selectedCharacter.name}
className="w-8 h-8 shrink-0"
iconClassName="h-4 w-4"
fallbackClassName="bg-[#FF5800]/10"
/>
) : (
<div className="w-8 h-8 rounded-full bg-[#FF5800]/20 border border-[#FF5800]/30 flex items-center justify-center shrink-0">
<Plus className="h-4 w-4 text-[#FF5800]" />
</div>
)}

{/* Character Info or Create New Agent */}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">
Expand Down
11 changes: 10 additions & 1 deletion lib/hooks/use-streaming-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ export async function sendStreamingMessage({
const decoder = new TextDecoder();
let buffer = "";

let completeCalled = false;

try {
while (true) {
const { done, value } = await reader.read();
Expand All @@ -191,13 +193,20 @@ export async function sendStreamingMessage({
onChunk,
onReasoning,
onError,
onComplete,
() => {
completeCalled = true;
onComplete?.();
},
);
} catch (err) {
console.error("[Stream] Error processing final buffer:", err);
onError?.("Stream ended unexpectedly");
}
}
// Always call onComplete when stream ends, if not already called
if (!completeCalled) {
onComplete?.();
}
Comment on lines +196 to +209
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onComplete callback can be called multiple times. While the new code adds a completeCalled flag to prevent duplicate calls, the flag is only set when wrapping onComplete for the final buffer processing (lines 196-199). However, when processSSEMessage is called in the regular message loop (line 236, which is outside the diff but calls onComplete directly), if a "done" event is received there, onComplete will be called without setting the completeCalled flag. This will cause onComplete to be called again at line 208. To fix this, the onComplete callback passed to processSSEMessage at line 236 should also use the same wrapper function.

Copilot uses AI. Check for mistakes.
break;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/privy-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import type { UserWithOrganization } from "@/lib/types";
import { getRandomUserAvatar } from "@/lib/utils/default-user-avatar";

const DEFAULT_INITIAL_CREDITS = 5.0;
const DEFAULT_INITIAL_CREDITS = 1.0;
const getInitialCredits = (): number => {
const envValue = process.env.INITIAL_FREE_CREDITS;
if (envValue) {
Expand Down
107 changes: 91 additions & 16 deletions lib/services/room-title.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { roomsRepository, memoriesRepository } from "@/db/repositories";
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { logger } from "@/lib/utils/logger";

/**
* Generate a title for a room based on the first user message.
* Generate an AI-powered title for a room based on the first user message.
* Only generates if room currently has default title ("New Chat").
*
* @param roomId - The room ID to generate title for
Expand All @@ -19,6 +21,7 @@ export async function generateRoomTitle(
}

if (room.name && room.name !== "New Chat") {
logger.info(`[RoomTitle] Room already has title: ${room.name}`);
return null;
}

Expand All @@ -45,25 +48,97 @@ export async function generateRoomTitle(
return null;
}

// Create title from first user message
const title = text
.replace(/\n/g, " ") // Remove newlines
.replace(/\s+/g, " ") // Collapse whitespace
.trim()
.substring(0, 40) // Limit to 40 chars
.trim();
// Generate AI title
let title: string;

try {
const prompt = `Create a brief 3-5 word title summarizing this message topic. Output ONLY the title, no quotes or explanation.

const finalTitle = text.length > 40 ? `${title}...` : title;
Message: ${text.slice(0, 300)}

if (!finalTitle || finalTitle.length < 3) {
return null;
Title:`;

logger.info(`[RoomTitle] Generating AI title for room ${roomId}`);

const result = await generateText({
model: gateway.languageModel("openai/gpt-4o-mini"),
Comment on lines +62 to +64
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AI title generation uses a hardcoded model "openai/gpt-4o-mini". Consider making this configurable through environment variables to allow flexibility in model selection and to avoid hardcoding infrastructure dependencies.

Suggested change
const result = await generateText({
model: gateway.languageModel("openai/gpt-4o-mini"),
const roomTitleModel =
process.env.ROOM_TITLE_MODEL || "openai/gpt-4o-mini";
const result = await generateText({
model: gateway.languageModel(roomTitleModel),

Copilot uses AI. Check for mistakes.
prompt,
});

// Clean up the generated title
title = result.text
.trim()
.replace(/^["']|["']$/g, "") // Remove quotes
.replace(/^Title:\s*/i, "") // Remove "Title:" prefix if present
.replace(/[.!?]$/, "") // Remove trailing punctuation
.split("\n")[0] // Take only first line
.slice(0, 50); // Limit length

logger.info(`[RoomTitle] AI generated: "${title}"`);

// Validate title is reasonable
if (!title || title.length < 3 || title.length > 50) {
logger.warn(`[RoomTitle] Invalid AI title, using fallback`);
title = generateFallbackTitle(text);
}
} catch (error) {
logger.error(`[RoomTitle] AI generation failed:`, error);
title = generateFallbackTitle(text);
}

await roomsRepository.update(roomId, { name: finalTitle });
await roomsRepository.update(roomId, { name: title });

logger.info(
`[RoomTitle] Generated title for room ${roomId}: "${finalTitle}"`,
);
logger.info(`[RoomTitle] Set title for room ${roomId}: "${title}"`);

return title;
}

/**
* Generate a descriptive title from the user message when AI fails.
*/
function generateFallbackTitle(message: string): string {
const cleaned = message.trim().toLowerCase();

// Common greeting patterns -> generic titles
if (/^(hi|hello|hey|howdy|greetings|yo|sup)/i.test(cleaned)) {
return "New Conversation";
}

// Question patterns
if (/^(what|how|why|when|where|who|can|could|would|should|is|are|do|does)/i.test(cleaned)) {
const words = message.trim().split(/\s+/).slice(0, 6);
if (words.length >= 3) {
return capitalizeFirst(words.slice(0, 5).join(" "));
Comment on lines +109 to +111
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback title generation logic at line 111 slices to 5 words but checks if words.length >= 3. This means messages with exactly 3 or 4 words will have all words included without ellipsis, but 5-word messages will also have all words without ellipsis (line 139 adds ellipsis only for messages with more than 5 words). Consider clarifying this logic or adjusting the threshold for consistency.

Suggested change
const words = message.trim().split(/\s+/).slice(0, 6);
if (words.length >= 3) {
return capitalizeFirst(words.slice(0, 5).join(" "));
const words = message.trim().split(/\s+/);
if (words.length >= 3) {
const titleWords = words.slice(0, 5);
const baseTitle = capitalizeFirst(
titleWords.join(" ").replace(/[.!?]+$/, ""),
);
return words.length > 5 ? baseTitle + "..." : baseTitle;

Copilot uses AI. Check for mistakes.
}
return "Question & Answer";
}

// Help/assist patterns
if (/^(help|assist|support|i need|please)/i.test(cleaned)) {
return "Help Request";
}

// Code/technical patterns
if (/^(code|write|create|build|make|implement|debug|fix)/i.test(cleaned)) {
return "Coding Assistance";
}

// Explain patterns
if (/^(explain|tell me|describe|what is|define)/i.test(cleaned)) {
return "Explanation Request";
}

// For other messages, extract first few meaningful words
const words = message.trim().split(/\s+/);
if (words.length <= 5) {
return capitalizeFirst(words.join(" ").replace(/[.!?]+$/, ""));
}

// Take first 5 words and capitalize
const title = words.slice(0, 5).join(" ");
return capitalizeFirst(title) + "...";
}

return finalTitle;
function capitalizeFirst(str: string): string {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capitalizeFirst helper function doesn't handle empty strings safely. If an empty string is passed, charAt(0) will return an empty string and slice(1) will also return empty, but this could be made more explicit with a guard check to improve robustness.

Suggested change
function capitalizeFirst(str: string): string {
function capitalizeFirst(str: string): string {
if (str.length === 0) {
return str;
}

Copilot uses AI. Check for mistakes.
return str.charAt(0).toUpperCase() + str.slice(1);
}
7 changes: 3 additions & 4 deletions lib/utils/character-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ export function generateDefaultCharacterName(): string {
}

/**
* Create a new default character with random name and avatar.
* Create a new default character with blank fields for user to fill in.
*/
export function createDefaultCharacter(): ElizaCharacter {
const name = generateDefaultCharacterName();
return {
name,
name: "",
username: "",
bio: "",
system: "",
topics: [],
Expand All @@ -80,6 +80,5 @@ export function createDefaultCharacter(): ElizaCharacter {
secrets: {},
style: {},
templates: {},
avatarUrl: generateDefaultAvatarUrl(name),
};
}
Loading