Skip to content
Open
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
1 change: 1 addition & 0 deletions web/app/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Claude Code local files
.claude/
web/app/tsconfig.tsbuildinfo
26 changes: 26 additions & 0 deletions web/app/env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# --- Firebase (Public Web Config ONLY) ---

NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=

# You MUST fetch these from Firebase Console -> Project Settings
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=

GEMINI_API_KEY=

# --- Push Notifications (Web Push / VAPID) ---
# Firebase Console -> Cloud Messaging -> Web Push certificates
NEXT_PUBLIC_FIREBASE_VAPID_KEY=

# --- Backend API Connection ---
# Map from API_BASE_URL / BASE_API_URL
NEXT_PUBLIC_API_BASE_URL=

NEXT_PUBLIC_WS_BASE_URL=

# --- Analytics (Optional) ---
NEXT_PUBLIC_MIXPANEL_TOKEN=
2 changes: 1 addition & 1 deletion web/app/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
97 changes: 97 additions & 0 deletions web/app/src/app/actions/proactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use server';

import { GeminiResponseSchema } from '@/lib/geminiClient';

interface AnalyzeScreenParams {
imageBase64: string;
imageMimeType?: string;
prompt: string;
systemPrompt: string;
responseSchema?: GeminiResponseSchema;
model?: string;
}

const DEFAULT_MODEL = 'gemini-2.0-flash';
const ALLOWED_MODELS = ['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro'];

export async function analyzeScreenAction(params: AnalyzeScreenParams): Promise<string> {
const apiKey = process.env.GEMINI_API_KEY;

if (!apiKey) {
throw new Error('Gemini API key not configured on server');
}

const {
imageBase64,
imageMimeType = 'image/jpeg',
prompt,
systemPrompt,
responseSchema,
model = DEFAULT_MODEL
} = params;

// Validate model
if (!ALLOWED_MODELS.includes(model)) {
throw new Error(`Invalid model specified. Allowed: ${ALLOWED_MODELS.join(', ')}`);
}

// Construct request payload
const requestBody: any = {
Comment thread
neooriginal marked this conversation as resolved.
contents: [
{
parts: [
{ text: prompt },
{
inline_data: {
mime_type: imageMimeType,
data: imageBase64,
},
},
],
},
],
system_instruction: {
parts: [{ text: systemPrompt }],
},
};

if (responseSchema) {
requestBody.generation_config = {
response_mime_type: 'application/json',
response_schema: responseSchema,
};
}

const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
Comment thread
neooriginal marked this conversation as resolved.

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
Comment thread
neooriginal marked this conversation as resolved.

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errorText}`);
}

const data = await response.json();

if (data.error) {
throw new Error(data.error.message);
}

const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
throw new Error('No text in response from Gemini API');
}

return text;
} catch (error: any) {
console.error('Server Action Analysis Failed:', error);
throw new Error(error.message || 'Analysis failed');
}
}
119 changes: 61 additions & 58 deletions web/app/src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ChatBubble } from '@/components/chat/ChatBubble';
import { BottomNavigation } from './BottomNavigation';
import { NotificationProvider, useNotificationContext } from '@/components/notifications/NotificationContext';
import { HeaderRecordingIndicator } from '@/components/recording';
import { ProactiveProvider } from '@/components/proactive';
import { getChatApps } from '@/lib/api';
import { cn } from '@/lib/utils';
import { MemoriesPrefetcher } from '@/components/memories/MemoriesPrefetcher';
Expand Down Expand Up @@ -82,70 +83,72 @@ export function MainLayout({ children, title, hideHeader = false }: MainLayoutPr
return (
<ChatProvider>
<NotificationProvider>
{/* Handle notification routing from chatApp query param */}
<ChatAppRouter />
{/* Prefetch memories in background for instant page load */}
<MemoriesPrefetcher />
<div className="h-screen w-screen bg-bg-primary flex overflow-hidden">
{/* Sidebar */}
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>

{/* Main content area - flex row to support push/slide panels */}
<div className="flex-1 flex min-w-0 h-full overflow-hidden">
{/* Main content */}
<main className="flex-1 flex flex-col min-w-0 h-full overflow-hidden pb-16 lg:pb-0">
{/* Header - conditionally shown */}
{!hideHeader && (
<header
className={cn(
'flex-shrink-0',
'flex items-center gap-4 px-4 py-4 lg:px-8',
'bg-bg-primary/80 backdrop-blur-md',
'border-b border-bg-tertiary'
)}
>
<MobileMenuButton onClick={() => setSidebarOpen(true)} />

{title && (
<h1 className="text-xl font-display font-semibold text-text-primary">
{title}
</h1>
)}
</header>
)}

{/* Mobile menu button when header is hidden */}
{hideHeader && (
<div className="lg:hidden absolute top-4 left-4 z-30">
<MobileMenuButton onClick={() => setSidebarOpen(true)} />
<ProactiveProvider>
{/* Handle notification routing from chatApp query param */}
<ChatAppRouter />
{/* Prefetch memories in background for instant page load */}
<MemoriesPrefetcher />
<div className="h-screen w-screen bg-bg-primary flex overflow-hidden">
{/* Sidebar */}
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>

{/* Main content area - flex row to support push/slide panels */}
<div className="flex-1 flex min-w-0 h-full overflow-hidden">
{/* Main content */}
<main className="flex-1 flex flex-col min-w-0 h-full overflow-hidden pb-16 lg:pb-0">
{/* Header - conditionally shown */}
{!hideHeader && (
<header
className={cn(
'flex-shrink-0',
'flex items-center gap-4 px-4 py-4 lg:px-8',
'bg-bg-primary/80 backdrop-blur-md',
'border-b border-bg-tertiary'
)}
>
<MobileMenuButton onClick={() => setSidebarOpen(true)} />

{title && (
<h1 className="text-xl font-display font-semibold text-text-primary">
{title}
</h1>
)}
</header>
)}

{/* Mobile menu button when header is hidden */}
{hideHeader && (
<div className="lg:hidden absolute top-4 left-4 z-30">
<MobileMenuButton onClick={() => setSidebarOpen(true)} />
</div>
)}

{/* Content */}
<div className="flex-1 overflow-hidden">
{children}
</div>
)}
</main>

{/* Content */}
<div className="flex-1 overflow-hidden">
{children}
</div>
</main>
{/* Chat panel - push/slide from right */}
<ChatPanel />

{/* Chat panel - push/slide from right */}
<ChatPanel />
{/* Notification center - push/slide from right */}
<NotificationCenter />
</div>

{/* Notification center - push/slide from right */}
<NotificationCenter />
</div>

{/* Chat bubble - floating button */}
<ChatBubble />
{/* Chat bubble - floating button */}
<ChatBubble />

{/* Bottom navigation - mobile only */}
<BottomNavigation onOpenSidebar={() => setSidebarOpen(true)} />
{/* Bottom navigation - mobile only */}
<BottomNavigation onOpenSidebar={() => setSidebarOpen(true)} />

{/* Recording indicator - handles its own fixed positioning and animates with panels */}
<HeaderRecordingIndicator />
</div>
{/* Recording indicator - handles its own fixed positioning and animates with panels */}
<HeaderRecordingIndicator />
</div>
</ProactiveProvider>
</NotificationProvider>
</ChatProvider>
);
Expand Down
24 changes: 5 additions & 19 deletions web/app/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,9 @@ import {
Bell,
Mic,
MessageSquare,
Smartphone,
Sparkles,
} from 'lucide-react';

// Apple logo SVG component
function AppleLogo({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
);
}

// Discord icon SVG component
function DiscordIcon({ className }: { className?: string }) {
return (
Expand Down Expand Up @@ -114,6 +105,7 @@ const settingsMenuItems = [
{ id: 'integrations', label: 'Integrations', icon: Puzzle },
{ id: 'developer', label: 'Developer', icon: Code },
{ id: 'account', label: 'Account', icon: Settings },
{ id: 'proactive', label: 'Proactive Assistant', icon: Sparkles },
];

interface SidebarProps {
Expand All @@ -134,7 +126,6 @@ export function Sidebar({
const [isExpanded, setIsExpanded] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const [isTemporaryExpand, setIsTemporaryExpand] = useState(false);
const [mobileAppDismissed, setMobileAppDismissed] = useState(false);
const isDesktop = useIsDesktop();
const sidebarRef = useRef<HTMLElement>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
Expand All @@ -145,9 +136,6 @@ export function Sidebar({
if (saved === 'true') {
setIsExpanded(true);
}
if (localStorage.getItem('mobile-app-banner-dismissed') === 'true') {
setMobileAppDismissed(true);
}
}, []);

// Click outside handler to close menu and collapse if temporary
Expand Down Expand Up @@ -254,11 +242,9 @@ export function Sidebar({
height={showText ? 24 : 13}
className="object-contain"
/>
{showText && (
<span className="text-[10px] bg-purple-primary/20 text-purple-primary px-1.5 py-0.5 rounded-full font-medium">
Beta
</span>
)}
<span className="text-[10px] bg-purple-primary/20 text-purple-primary px-1.5 py-0.5 rounded-full font-medium">
Beta
</span>
</Link>

{/* Mobile close button */}
Expand Down
Loading