From 29fe5e048e5e10f33f44913bca1459b76aa54dd2 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Sat, 9 May 2026 21:47:17 +0100 Subject: [PATCH 001/165] fix(expo): update Google sign-in logic to use new authClient method --- apps/expo/features/auth/hooks/useAuthActions.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/expo/features/auth/hooks/useAuthActions.ts b/apps/expo/features/auth/hooks/useAuthActions.ts index 6cfefe8819..56881e116b 100644 --- a/apps/expo/features/auth/hooks/useAuthActions.ts +++ b/apps/expo/features/auth/hooks/useAuthActions.ts @@ -88,17 +88,12 @@ export function useAuthActions() { if (!idToken) throw new Error(t('auth.noIdTokenFromGoogle')); - const { data, error } = await apiClient.auth.google.post({ idToken }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToSignInWithGoogle'))); - } - - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - userStore.set(UserSchema.parse(data.user)); - - setNeedsReauth(false); - redirect(redirectTo); + const { data, error } = await authClient.signIn.social({ + provider: 'google', + idToken: { token: idToken }, + }); + if (error) throw new Error(error.message ?? t('auth.failedToSignInWithGoogle')); + if (data && 'user' in data && data.user) applySession(data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } catch (error) { setIsLoading(false); From 2e2377e12d78aa343b2351068bb85da9a4b23ec2 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Thu, 7 May 2026 22:37:50 -0600 Subject: [PATCH 002/165] feat(web): wire gear-inventory and ai-chat screens to real API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GearInventoryScreen: replaces mock data with usePacks(); deduplicates items across packs, filters deleted, derives categories dynamically - AIScreen: replaces MOCK_RESPONSES with DefaultChatTransport hitting /api/chat; uses sendMessage/status from @ai-sdk/react v3 API - Adds @ai-sdk/react and ai to apps/web deps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/components/screens/ai-screen.tsx | 289 +++--------------- .../screens/gear-inventory-screen.tsx | 165 +++++----- apps/web/package.json | 2 + 3 files changed, 128 insertions(+), 328 deletions(-) diff --git a/apps/web/components/screens/ai-screen.tsx b/apps/web/components/screens/ai-screen.tsx index 5dc8be08e9..75227966d5 100644 --- a/apps/web/components/screens/ai-screen.tsx +++ b/apps/web/components/screens/ai-screen.tsx @@ -1,22 +1,13 @@ 'use client'; -import { Bot, Plus, Send, User } from 'lucide-react'; +import { type UIMessage, useChat } from '@ai-sdk/react'; +import { webEnv } from '@packrat/env/web'; +import { DefaultChatTransport, type TextUIPart } from 'ai'; +import Cookies from 'js-cookie'; +import { Bot, Send, User } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { cn } from 'web-app/lib/utils'; import { useWeight } from 'web-app/lib/weight-context'; -interface Message { - id: string; - role: 'user' | 'assistant'; - content: string; - packSuggestion?: PackSuggestion; - toolCalls?: string[]; -} - -interface PackSuggestion { - name: string; - items: { name: string; brand: string; weight: number; category: string }[]; -} - const STARTER_PROMPTS = [ 'Build me a 3-season PCT shelter kit under 2kg', 'Optimize my current pack', @@ -24,126 +15,36 @@ const STARTER_PROMPTS = [ 'Best ultralight tarp vs tent options?', ]; -const MOCK_RESPONSES: Record< - string, - { content: string; toolCalls: string[]; packSuggestion?: PackSuggestion } -> = { - 'Build me a 3-season PCT shelter kit under 2kg': { - toolCalls: ['🔍 Searching catalog…', '⚖️ Calculating weights…', '📋 Building pack suggestion…'], - content: - "Here's a 3-season PCT shelter kit optimized for under 2kg. This setup covers everything from desert heat to unexpected mountain snow, keeping you protected without sacrificing too much weight.", - packSuggestion: { - name: '3-Season PCT Shelter Kit', - items: [ - { name: 'Solplex Tent', brand: 'Zpacks', weight: 340, category: 'Shelter' }, - { - name: 'Revelation 20° Quilt', - brand: 'Enlightened Equipment', - weight: 397, - category: 'Sleep System', - }, - { - name: 'NeoAir XLite Max SV', - brand: 'Therm-a-Rest', - weight: 340, - category: 'Sleep System', - }, - { - name: 'Sea to Summit Stuff Sack 4L', - brand: 'Sea to Summit', - weight: 25, - category: 'Other', - }, - ], - }, - }, - 'Optimize my current pack': { - toolCalls: [ - '⚖️ Analyzing current pack…', - '🧠 Finding optimizations…', - '📊 Ranking suggestions…', - ], - content: - 'I analyzed your **3-Season PCT Thru-Hike** pack (6.8 lbs base). Here are 3 high-impact swaps to bring you under 5 lbs:\n\n• **Swap shelter**: Copper Spur HV UL2 → Zpacks Solplex saves **487g**\n• **Swap sleep**: Ohm sleeping bag → EE Revelation Quilt saves **283g**\n• **Swap pack**: Osprey Exos 58 → Zpacks Arc Blast saves **610g**\n\nTotal savings: ~1.38 lbs — bringing you to **5.4 lbs base weight**.', - }, - 'What gear do I need for a 20°F trip?': { - toolCalls: ['🌤 Checking weather data…', '🔍 Searching catalog…', '⚖️ Calculating weights…'], - content: - "For a 20°F trip you'll need a sleep system rated to at least 15°F for comfort, insulated layers, a 4-season or convertible shelter, and waterproof outerwear. Here's a recommended kit:", - packSuggestion: { - name: '20°F Winter Kit', - items: [ - { - name: 'Ohm 20° Sleeping Bag', - brand: 'Western Mountaineering', - weight: 680, - category: 'Sleep System', - }, - { - name: 'NeoAir XLite Max SV', - brand: 'Therm-a-Rest', - weight: 340, - category: 'Sleep System', - }, - { name: 'Nano Puff Jacket', brand: 'Patagonia', weight: 312, category: 'Clothing' }, - { name: 'Altaplex Tent', brand: 'Zpacks', weight: 454, category: 'Shelter' }, - { name: 'inReach Mini 2', brand: 'Garmin', weight: 100, category: 'Navigation' }, - ], - }, - }, - 'Best ultralight tarp vs tent options?': { - toolCalls: ['🔍 Searching catalog…', '📊 Comparing options…'], - content: - "Great question! Here's the quick breakdown:\n\n**Tarps** (e.g., Zpacks tarp ~180g): Lightest option, excellent ventilation, maximally versatile — but require skill to pitch well and offer less bug protection.\n\n**Single-wall tents** (e.g., Zpacks Solplex 340g): Best balance of weight, weatherproofness, and ease. Small condensation trade-off.\n\n**Double-wall tents** (e.g., Big Agnes Copper Spur ~997g): Most comfortable, best condensation management, but heaviest.\n\n**My recommendation**: For PCT thru-hiking, the Zpacks Solplex hits the sweet spot at 340g.", - }, -}; +const API_BASE = webEnv.NEXT_PUBLIC_API_URL ?? 'http://localhost:8787'; -const DEFAULT_RESPONSE: { content: string; toolCalls: string[]; packSuggestion?: PackSuggestion } = - { - toolCalls: ['🔍 Searching catalog…', '🧠 Generating response…'], - content: - "That's a great question! I can help you plan, optimize, and gear up for any adventure. Try asking me to build a specific kit, optimize a pack, or recommend gear for particular conditions.", - }; +function getTextContent(msg: UIMessage): string { + return msg.parts.find((p): p is TextUIPart => p.type === 'text')?.text ?? ''; +} export function AIScreen() { - const { fw } = useWeight(); - const [messages, setMessages] = useState([]); + const { fw: _fw } = useWeight(); const [input, setInput] = useState(''); - const [isTyping, setIsTyping] = useState(false); - const [activeTools, setActiveTools] = useState([]); const bottomRef = useRef(null); + const transport = new DefaultChatTransport({ + api: `${API_BASE}/api/chat`, + headers: () => ({ Authorization: `Bearer ${Cookies.get('access_token') ?? ''}` }), + body: () => ({ date: new Date().toISOString() }), + }); + + const { messages, sendMessage, status } = useChat({ transport }); + + const isTyping = status === 'submitted' || status === 'streaming'; + + // biome-ignore lint/correctness/useExhaustiveDependencies: scroll to bottom when message count changes useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, []); + }, [messages.length]); - const sendMessage = async (text: string) => { + const submit = (text: string) => { if (!text.trim() || isTyping) return; - const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text }; - setMessages((m) => [...m, userMsg]); + sendMessage({ text: text.trim() }); setInput(''); - setIsTyping(true); - - const response = MOCK_RESPONSES[text] ?? DEFAULT_RESPONSE; - - // Simulate tool calls - for (const tool of response.toolCalls) { - setActiveTools([tool]); - await sleep(600); - } - setActiveTools([]); - - // Simulate streaming - await sleep(300); - const assistantMsg: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: response.content, - packSuggestion: response.packSuggestion, - toolCalls: response.toolCalls, - }; - setMessages((m) => [...m, assistantMsg]); - setIsTyping(false); }; return ( @@ -183,7 +84,7 @@ export function AIScreen() {