diff --git a/backend/internal/ai/gemini.go b/backend/internal/ai/gemini.go index 4cddd9c..d736bc8 100644 --- a/backend/internal/ai/gemini.go +++ b/backend/internal/ai/gemini.go @@ -74,11 +74,43 @@ func (p *geminiProvider) Summarize(ctx context.Context, prompt string, _ GenOpti return result.Candidates[0].Content.Parts[0].Text, nil } -func (p *geminiProvider) ListModels(_ context.Context) ([]string, error) { - return []string{ - "gemini-2.0-flash", - "gemini-2.0-flash-lite", - "gemini-1.5-pro", - "gemini-1.5-flash", - }, nil +func (p *geminiProvider) ListModels(ctx context.Context) ([]string, error) { + url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models?key=%s", p.apiKey) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := p.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("gemini list models error %d: %s", resp.StatusCode, raw) + } + var result struct { + Models []struct { + Name string `json:"name"` + SupportedMethods []string `json:"supportedGenerationMethods"` + } `json:"models"` + } + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + var names []string + for _, m := range result.Models { + for _, method := range m.SupportedMethods { + if method == "generateContent" { + // name is "models/gemini-xxx", strip prefix + id := m.Name + if len(id) > 7 { + id = id[7:] // strip "models/" + } + names = append(names, id) + break + } + } + } + return names, nil } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 7185b80..c49ab17 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -15,7 +15,7 @@ function useTextSelection(containerRef: React.RefObject) { const [selection, setSelection] = useState<{ text: string; x: number; y: number } | null>(null) useEffect(() => { - function onMouseUp() { + function readSelection() { const sel = window.getSelection() const text = sel?.toString().trim() if (!text || !containerRef.current) { setSelection(null); return } @@ -28,14 +28,23 @@ function useTextSelection(containerRef: React.RefObject) { y: rect.top - containerRect.top - 8, }) } - function onMouseDown(e: MouseEvent) { + function onMouseUp() { readSelection() } + // On mobile, selectionchange fires after the user lifts their finger + function onSelectionChange() { + const sel = window.getSelection() + if (!sel?.toString().trim()) setSelection(null) + else readSelection() + } + function onPointerDown(e: PointerEvent) { if (!(e.target as Element).closest('[data-context-action]')) setSelection(null) } document.addEventListener('mouseup', onMouseUp) - document.addEventListener('mousedown', onMouseDown) + document.addEventListener('selectionchange', onSelectionChange) + document.addEventListener('pointerdown', onPointerDown) return () => { document.removeEventListener('mouseup', onMouseUp) - document.removeEventListener('mousedown', onMouseDown) + document.removeEventListener('selectionchange', onSelectionChange) + document.removeEventListener('pointerdown', onPointerDown) } }, [containerRef]) @@ -278,11 +287,12 @@ export function Dashboard() {
+ {/* Desktop: floating button above selection */} {textSel && ( )}
+ {/* Mobile: fixed bottom bar — avoids conflict with native selection menu */} + {textSel && ( +
+ +
+ )}

Sélectionnez du texte pour l'ajouter au contexte, puis posez une question dans le panneau qui apparaît.