Skip to content

Commit 166211d

Browse files
author
catlog22
committed
feat: add Unsplash search hook and API proxy routes
- Implemented `useUnsplashSearch` hook for searching Unsplash photos with debounce. - Created Unsplash API client functions for searching photos and triggering downloads. - Added proxy routes for Unsplash API to handle search requests and background image uploads. - Introduced accessibility utilities for WCAG compliance checks and motion preference management. - Developed theme sharing module for encoding and decoding theme configurations as base64url strings.
1 parent 87daccd commit 166211d

52 files changed

Lines changed: 5798 additions & 142 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ccw/frontend/src/components/a2ui/A2UIPopupCard.tsx

Lines changed: 388 additions & 5 deletions
Large diffs are not rendered by default.

ccw/frontend/src/components/codexlens/ModelCard.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function ModelCard({
6868
};
6969

7070
return (
71-
<Card className={cn('overflow-hidden', !model.installed && 'opacity-80')}>
71+
<Card className={cn('overflow-hidden hover-glow', !model.installed && 'opacity-80')}>
7272
{/* Header */}
7373
<div className="p-4">
7474
<div className="flex items-start justify-between gap-3">
@@ -105,12 +105,15 @@ export function ModelCard({
105105
</Badge>
106106
</div>
107107
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
108-
<span>Backend: {model.backend}</span>
109-
<span>Size: {formatSize(model.size)}</span>
108+
{model.dimensions && <span>{model.dimensions}d</span>}
109+
<span>{formatSize(model.size)}</span>
110+
{model.recommended && (
111+
<Badge variant="success" className="text-[10px] px-1 py-0">Rec</Badge>
112+
)}
110113
</div>
111-
{model.cache_path && (
112-
<p className="text-xs text-muted-foreground mt-1 font-mono truncate">
113-
{model.cache_path}
114+
{model.description && (
115+
<p className="text-xs text-muted-foreground mt-1">
116+
{model.description}
114117
</p>
115118
)}
116119
</div>

ccw/frontend/src/components/codexlens/ModelsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function filterModels(models: CodexLensModel[], filter: FilterType, search: stri
4747
filtered = filtered.filter(m =>
4848
m.name.toLowerCase().includes(query) ||
4949
m.profile.toLowerCase().includes(query) ||
50-
m.backend.toLowerCase().includes(query)
50+
(m.description?.toLowerCase().includes(query) ?? false)
5151
);
5252
}
5353

ccw/frontend/src/components/codexlens/SearchTab.tsx

Lines changed: 163 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
// CodexLens Search Tab
33
// ========================================
44
// Semantic code search interface with multiple search types
5+
// Includes LSP availability check and hybrid search mode switching
56

67
import { useState } from 'react';
78
import { useIntl } from 'react-intl';
8-
import { Search, FileCode, Code } from 'lucide-react';
9+
import { Search, FileCode, Code, Sparkles, CheckCircle, AlertTriangle } from 'lucide-react';
910
import { Button } from '@/components/ui/Button';
1011
import { Input } from '@/components/ui/Input';
1112
import { Label } from '@/components/ui/Label';
@@ -20,11 +21,13 @@ import {
2021
useCodexLensSearch,
2122
useCodexLensFilesSearch,
2223
useCodexLensSymbolSearch,
24+
useCodexLensLspStatus,
25+
useCodexLensSemanticSearch,
2326
} from '@/hooks/useCodexLens';
24-
import type { CodexLensSearchParams } from '@/lib/api';
27+
import type { CodexLensSearchParams, CodexLensSemanticSearchMode, CodexLensFusionStrategy } from '@/lib/api';
2528
import { cn } from '@/lib/utils';
2629

27-
type SearchType = 'search' | 'search_files' | 'symbol';
30+
type SearchType = 'search' | 'search_files' | 'symbol' | 'semantic';
2831
type SearchMode = 'dense_rerank' | 'fts' | 'fuzzy';
2932

3033
interface SearchTabProps {
@@ -35,14 +38,19 @@ export function SearchTab({ enabled }: SearchTabProps) {
3538
const { formatMessage } = useIntl();
3639
const [searchType, setSearchType] = useState<SearchType>('search');
3740
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
41+
const [semanticMode, setSemanticMode] = useState<CodexLensSemanticSearchMode>('fusion');
42+
const [fusionStrategy, setFusionStrategy] = useState<CodexLensFusionStrategy>('rrf');
3843
const [query, setQuery] = useState('');
3944
const [hasSearched, setHasSearched] = useState(false);
4045

46+
// LSP status check
47+
const lspStatus = useCodexLensLspStatus({ enabled });
48+
4149
// Build search params based on search type
4250
const searchParams: CodexLensSearchParams = {
4351
query,
4452
limit: 20,
45-
mode: searchType !== 'symbol' ? searchMode : undefined,
53+
mode: searchType !== 'symbol' && searchType !== 'semantic' ? searchMode : undefined,
4654
max_content_length: 200,
4755
extra_files_count: 10,
4856
};
@@ -63,12 +71,25 @@ export function SearchTab({ enabled }: SearchTabProps) {
6371
{ enabled: enabled && hasSearched && searchType === 'symbol' && query.trim().length > 0 }
6472
);
6573

74+
const semanticSearch = useCodexLensSemanticSearch(
75+
{
76+
query,
77+
mode: semanticMode,
78+
fusion_strategy: semanticMode === 'fusion' ? fusionStrategy : undefined,
79+
limit: 20,
80+
include_match_reason: true,
81+
},
82+
{ enabled: enabled && hasSearched && searchType === 'semantic' && query.trim().length > 0 }
83+
);
84+
6685
// Get loading state based on search type
6786
const isLoading = searchType === 'search'
6887
? contentSearch.isLoading
6988
: searchType === 'search_files'
7089
? fileSearch.isLoading
71-
: symbolSearch.isLoading;
90+
: searchType === 'symbol'
91+
? symbolSearch.isLoading
92+
: semanticSearch.isLoading;
7293

7394
const handleSearch = () => {
7495
if (query.trim()) {
@@ -84,17 +105,52 @@ export function SearchTab({ enabled }: SearchTabProps) {
84105

85106
const handleSearchTypeChange = (value: SearchType) => {
86107
setSearchType(value);
87-
setHasSearched(false); // Reset search state when changing type
108+
setHasSearched(false);
88109
};
89110

90111
const handleSearchModeChange = (value: SearchMode) => {
91112
setSearchMode(value);
92-
setHasSearched(false); // Reset search state when changing mode
113+
setHasSearched(false);
114+
};
115+
116+
const handleSemanticModeChange = (value: CodexLensSemanticSearchMode) => {
117+
setSemanticMode(value);
118+
setHasSearched(false);
119+
};
120+
121+
const handleFusionStrategyChange = (value: CodexLensFusionStrategy) => {
122+
setFusionStrategy(value);
123+
setHasSearched(false);
93124
};
94125

95126
const handleQueryChange = (value: string) => {
96127
setQuery(value);
97-
setHasSearched(false); // Reset search state when query changes
128+
setHasSearched(false);
129+
};
130+
131+
// Get result count for display
132+
const getResultCount = (): string => {
133+
if (searchType === 'symbol') {
134+
return symbolSearch.data?.success
135+
? `${symbolSearch.data.symbols?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
136+
: '';
137+
}
138+
if (searchType === 'search') {
139+
return contentSearch.data?.success
140+
? `${contentSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
141+
: '';
142+
}
143+
if (searchType === 'search_files') {
144+
return fileSearch.data?.success
145+
? `${fileSearch.data.files?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
146+
: '';
147+
}
148+
if (searchType === 'semantic') {
149+
return semanticSearch.data?.success
150+
? `${semanticSearch.data.count ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
151+
: '';
152+
}
153+
return '';
98154
};
99155

100156
if (!enabled) {
@@ -115,6 +171,29 @@ export function SearchTab({ enabled }: SearchTabProps) {
115171

116172
return (
117173
<div className="space-y-6">
174+
{/* LSP Status Indicator */}
175+
<div className="flex items-center gap-2 text-sm">
176+
<span className="text-muted-foreground">{formatMessage({ id: 'codexlens.search.lspStatus' })}:</span>
177+
{lspStatus.isLoading ? (
178+
<span className="text-muted-foreground">...</span>
179+
) : lspStatus.available ? (
180+
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
181+
<CheckCircle className="w-3.5 h-3.5" />
182+
{formatMessage({ id: 'codexlens.search.lspAvailable' })}
183+
</span>
184+
) : !lspStatus.semanticAvailable ? (
185+
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
186+
<AlertTriangle className="w-3.5 h-3.5" />
187+
{formatMessage({ id: 'codexlens.search.lspNoSemantic' })}
188+
</span>
189+
) : (
190+
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
191+
<AlertTriangle className="w-3.5 h-3.5" />
192+
{formatMessage({ id: 'codexlens.search.lspNoVector' })}
193+
</span>
194+
)}
195+
</div>
196+
118197
{/* Search Options */}
119198
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
120199
{/* Search Type */}
@@ -143,12 +222,18 @@ export function SearchTab({ enabled }: SearchTabProps) {
143222
{formatMessage({ id: 'codexlens.search.symbol' })}
144223
</div>
145224
</SelectItem>
225+
<SelectItem value="semantic" disabled={!lspStatus.available}>
226+
<div className="flex items-center gap-2">
227+
<Sparkles className="w-4 h-4" />
228+
{formatMessage({ id: 'codexlens.search.semantic' })}
229+
</div>
230+
</SelectItem>
146231
</SelectContent>
147232
</Select>
148233
</div>
149234

150-
{/* Search Mode - only for content and file search */}
151-
{searchType !== 'symbol' && (
235+
{/* Search Mode - for CLI search types (content / file) */}
236+
{(searchType === 'search' || searchType === 'search_files') && (
152237
<div className="space-y-2">
153238
<Label>{formatMessage({ id: 'codexlens.search.mode' })}</Label>
154239
<Select value={searchMode} onValueChange={handleSearchModeChange}>
@@ -169,8 +254,60 @@ export function SearchTab({ enabled }: SearchTabProps) {
169254
</Select>
170255
</div>
171256
)}
257+
258+
{/* Semantic Search Mode - for semantic search type */}
259+
{searchType === 'semantic' && (
260+
<div className="space-y-2">
261+
<Label>{formatMessage({ id: 'codexlens.search.semanticMode' })}</Label>
262+
<Select value={semanticMode} onValueChange={handleSemanticModeChange}>
263+
<SelectTrigger>
264+
<SelectValue />
265+
</SelectTrigger>
266+
<SelectContent>
267+
<SelectItem value="fusion">
268+
{formatMessage({ id: 'codexlens.search.semanticMode.fusion' })}
269+
</SelectItem>
270+
<SelectItem value="vector">
271+
{formatMessage({ id: 'codexlens.search.semanticMode.vector' })}
272+
</SelectItem>
273+
<SelectItem value="structural">
274+
{formatMessage({ id: 'codexlens.search.semanticMode.structural' })}
275+
</SelectItem>
276+
</SelectContent>
277+
</Select>
278+
</div>
279+
)}
172280
</div>
173281

282+
{/* Fusion Strategy - only when semantic + fusion mode */}
283+
{searchType === 'semantic' && semanticMode === 'fusion' && (
284+
<div className="space-y-2">
285+
<Label>{formatMessage({ id: 'codexlens.search.fusionStrategy' })}</Label>
286+
<Select value={fusionStrategy} onValueChange={handleFusionStrategyChange}>
287+
<SelectTrigger>
288+
<SelectValue />
289+
</SelectTrigger>
290+
<SelectContent>
291+
<SelectItem value="rrf">
292+
{formatMessage({ id: 'codexlens.search.fusionStrategy.rrf' })}
293+
</SelectItem>
294+
<SelectItem value="dense_rerank">
295+
{formatMessage({ id: 'codexlens.search.fusionStrategy.dense_rerank' })}
296+
</SelectItem>
297+
<SelectItem value="binary">
298+
{formatMessage({ id: 'codexlens.search.fusionStrategy.binary' })}
299+
</SelectItem>
300+
<SelectItem value="hybrid">
301+
{formatMessage({ id: 'codexlens.search.fusionStrategy.hybrid' })}
302+
</SelectItem>
303+
<SelectItem value="staged">
304+
{formatMessage({ id: 'codexlens.search.fusionStrategy.staged' })}
305+
</SelectItem>
306+
</SelectContent>
307+
</Select>
308+
</div>
309+
)}
310+
174311
{/* Query Input */}
175312
<div className="space-y-2">
176313
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>
@@ -205,21 +342,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
205342
{formatMessage({ id: 'codexlens.search.results' })}
206343
</h3>
207344
<span className="text-xs text-muted-foreground">
208-
{searchType === 'symbol'
209-
? (symbolSearch.data?.success
210-
? `${symbolSearch.data.symbols?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
211-
: ''
212-
)
213-
: searchType === 'search'
214-
? (contentSearch.data?.success
215-
? `${contentSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
216-
: ''
217-
)
218-
: (fileSearch.data?.success
219-
? `${fileSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
220-
: ''
221-
)
222-
}
345+
{getResultCount()}
223346
</span>
224347
</div>
225348

@@ -255,7 +378,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
255378
fileSearch.data.success ? (
256379
<div className="rounded-lg border bg-muted/50 p-4">
257380
<pre className="text-xs overflow-auto max-h-96">
258-
{JSON.stringify(fileSearch.data.results, null, 2)}
381+
{JSON.stringify(fileSearch.data.files, null, 2)}
259382
</pre>
260383
</div>
261384
) : (
@@ -264,6 +387,20 @@ export function SearchTab({ enabled }: SearchTabProps) {
264387
</div>
265388
)
266389
)}
390+
391+
{searchType === 'semantic' && semanticSearch.data && (
392+
semanticSearch.data.success ? (
393+
<div className="rounded-lg border bg-muted/50 p-4">
394+
<pre className="text-xs overflow-auto max-h-96">
395+
{JSON.stringify(semanticSearch.data.results, null, 2)}
396+
</pre>
397+
</div>
398+
) : (
399+
<div className="text-sm text-destructive">
400+
{semanticSearch.data.error || formatMessage({ id: 'common.error' })}
401+
</div>
402+
)
403+
)}
267404
</div>
268405
)}
269406
</div>

ccw/frontend/src/components/dashboard/DashboardHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function DashboardHeader({
4343
return (
4444
<div className="flex items-center justify-between">
4545
<div>
46-
<h1 className="text-2xl font-semibold text-foreground">
46+
<h1 className="text-2xl font-semibold text-foreground gradient-text">
4747
{formatMessage({ id: titleKey })}
4848
</h1>
4949
<p className="text-sm text-muted-foreground mt-1">

ccw/frontend/src/components/dashboard/widgets/WorkflowTaskWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
253253
return (
254254
<div className={cn('flex flex-col gap-2', className)}>
255255
{/* Project Info Banner - Separate Card */}
256-
<Card className="shrink-0">
256+
<Card className="shrink-0 border-gradient-brand">
257257
{projectLoading ? (
258258
<div className="px-4 py-3 flex items-center gap-4">
259259
<div className="h-5 w-32 bg-muted rounded animate-pulse" />

ccw/frontend/src/components/layout/AppShell.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MainContent } from './MainContent';
1212
import { CliStreamMonitor } from '@/components/shared/CliStreamMonitor';
1313
import { NotificationPanel } from '@/components/notification';
1414
import { AskQuestionDialog, A2UIPopupCard } from '@/components/a2ui';
15+
import { BackgroundImage } from '@/components/shared/BackgroundImage';
1516
import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores';
1617
import { useWorkflowStore } from '@/stores/workflowStore';
1718
import { useWebSocketNotifications, useWebSocket } from '@/hooks';
@@ -160,6 +161,9 @@ export function AppShell({
160161

161162
return (
162163
<div className="flex flex-col min-h-screen bg-background">
164+
{/* Background image layer (z-index: -3 to -2) */}
165+
<BackgroundImage />
166+
163167
{/* Header - fixed at top */}
164168
<Header
165169
onRefresh={onRefresh}
@@ -180,7 +184,7 @@ export function AppShell({
180184
{/* Main content area */}
181185
<MainContent
182186
className={cn(
183-
'transition-all duration-300',
187+
'app-shell-content transition-all duration-300',
184188
sidebarCollapsed ? 'md:ml-16' : 'md:ml-64'
185189
)}
186190
>

ccw/frontend/src/components/layout/Header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function Header({
5959

6060
return (
6161
<header
62-
className="flex items-center justify-between px-4 md:px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm"
62+
className="relative flex items-center justify-between px-4 md:px-5 h-14 bg-card border-b border-border sticky top-0 z-50 shadow-sm"
6363
role="banner"
6464
>
6565
{/* Left side - Logo */}
@@ -200,6 +200,7 @@ export function Header({
200200
</div>
201201
</div>
202202
</div>
203+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-accent" aria-hidden="true" />
203204
</header>
204205
);
205206
}

0 commit comments

Comments
 (0)