-
-
Save getflourish/5187557b9bee23d08f2d49e076a6e2ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from "react"; | |
| import { GoogleGenAI } from "@google/genai"; | |
| export default function CapitalizationDemo() { | |
| const textareaRef = React.useRef(null); | |
| const [apiKey, setApiKey] = React.useState(""); | |
| const [model, setModel] = React.useState("gemini-2.5-flash-lite"); | |
| const [text, setText] = React.useState(""); | |
| const [isProcessing, setIsProcessing] = React.useState(false); | |
| const [error, setError] = React.useState(""); | |
| const ensureTrailingNewline = React.useCallback((value) => { | |
| return value.endsWith("\n") ? value : value + "\n"; | |
| }, []); | |
| React.useEffect(() => { | |
| try { | |
| const savedKey = window.localStorage.getItem("CAPITALIZER_API_KEY"); | |
| if (savedKey) setApiKey(savedKey); | |
| } catch {} | |
| }, []); | |
| React.useEffect(() => { | |
| try { | |
| if (apiKey) { | |
| window.localStorage.setItem("CAPITALIZER_API_KEY", apiKey); | |
| } | |
| } catch {} | |
| }, [apiKey]); | |
| function createClient() { | |
| if (!apiKey) throw new Error("Missing API key"); | |
| return new GoogleGenAI({ apiKey }); | |
| } | |
| async function capitalizeViaAI(input) { | |
| const ai = createClient(); | |
| const systemInstruction = | |
| "You are a precise capitalization and punctuation fixer. You receive lowercase or inconsistently capitalized sentences in German or English. Return the same content with correct capitalization and punctuation, preserving meaning and formatting as much as possible. Do not add new content."; | |
| const response = await ai.models.generateContent({ | |
| model, | |
| contents: input, | |
| config: { | |
| systemInstruction, | |
| }, | |
| }); | |
| return response.text; | |
| } | |
| function getCaretInfo() { | |
| const el = textareaRef.current; | |
| if (!el) return { start: 0, end: 0 }; | |
| return { start: el.selectionStart, end: el.selectionEnd }; | |
| } | |
| function setCaret(posStart, posEnd) { | |
| const el = textareaRef.current; | |
| if (!el) return; | |
| el.focus(); | |
| el.setSelectionRange(posStart, posEnd); | |
| } | |
| function getLineIndexAtPosition(value, pos) { | |
| const upToPos = value.slice(0, pos); | |
| return upToPos.split("\n").length - 1; | |
| } | |
| function getLineBounds(value, lineIndex) { | |
| let start = 0; | |
| for (let i = 0, count = 0; i < value.length; i++) { | |
| if (count === lineIndex) { | |
| start = i; | |
| break; | |
| } | |
| if (value[i] === "\n") count++; | |
| } | |
| // If target line is first line | |
| if (lineIndex === 0) start = 0; | |
| let end = value.indexOf("\n", start); | |
| if (end === -1) end = value.length; | |
| return { start, end }; | |
| } | |
| async function processLineAtIndex(lineIndex) { | |
| if (lineIndex < 0) return; | |
| setError(""); | |
| setIsProcessing(true); | |
| try { | |
| const { start, end } = getLineBounds(text, lineIndex); | |
| const originalLine = text.slice(start, end); | |
| const fixed = await capitalizeViaAI(originalLine); | |
| const needsNewline = text[end] !== "\n"; | |
| const replacement = needsNewline ? fixed + "\n" : fixed; | |
| const newText = | |
| text.slice(0, start) + | |
| replacement + | |
| text.slice(end + (needsNewline ? 0 : 0)); | |
| // Compute caret offset change based on where caret currently is | |
| const { start: caretStart, end: caretEnd } = getCaretInfo(); | |
| const delta = replacement.length - (end - start); | |
| setText(ensureTrailingNewline(newText)); | |
| // Restore caret at same logical place relative to the edited segment | |
| const afterUpdate = () => { | |
| // If caret was after the edited line end, shift by delta | |
| if (caretStart >= end) { | |
| setCaret(caretStart + delta, caretEnd + delta); | |
| } else { | |
| // Keep caret as close as possible to original position | |
| setCaret(caretStart, caretEnd); | |
| } | |
| }; | |
| // Wait next tick for DOM to reflect new value | |
| requestAnimationFrame(afterUpdate); | |
| } catch (e) { | |
| setError(e?.message || String(e)); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| } | |
| function handleKeyDown(e) { | |
| if (e.key !== "Enter") return; | |
| // Let the newline be inserted, then process the previous line | |
| const { start } = getCaretInfo(); | |
| const lineNow = getLineIndexAtPosition(text, start); | |
| const previousLine = lineNow; | |
| if (previousLine < 0) return; | |
| // Defer processing until after textarea updates with new line break | |
| setTimeout(() => { | |
| processLineAtIndex(previousLine); | |
| }, 0); | |
| } | |
| return ( | |
| <div style={{ display: "grid", gap: 12 }}> | |
| <div style={{ display: "grid", gap: 8 }}> | |
| <label style={{ fontWeight: 600 }}> | |
| API Key | |
| <input | |
| type="password" | |
| value={apiKey} | |
| onChange={(e) => setApiKey(e.target.value)} | |
| placeholder="Enter Google AI Studio API key" | |
| style={{ | |
| width: "100%", | |
| padding: "8px 10px", | |
| border: "1px solid #ccc", | |
| borderRadius: 6, | |
| marginTop: 6, | |
| fontFamily: "inherit", | |
| fontSize: 14, | |
| }} | |
| /> | |
| </label> | |
| <label style={{ fontWeight: 600 }}> | |
| Model | |
| <select | |
| value={model} | |
| onChange={(e) => setModel(e.target.value)} | |
| style={{ | |
| width: "100%", | |
| padding: "8px 10px", | |
| border: "1px solid #ccc", | |
| borderRadius: 6, | |
| marginTop: 6, | |
| fontFamily: "inherit", | |
| fontSize: 14, | |
| background: "white", | |
| }} | |
| > | |
| <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> | |
| <option value="gemini-2.5-flash">gemini-2.5-flash</option> | |
| </select> | |
| </label> | |
| </div> | |
| <textarea | |
| ref={textareaRef} | |
| value={text} | |
| onChange={(e) => setText(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={ | |
| "Type in lowercase. Press Enter to auto-capitalize the previous line." | |
| } | |
| rows={10} | |
| style={{ | |
| width: "100%", | |
| resize: "vertical", | |
| padding: 12, | |
| border: "1px solid #ccc", | |
| borderRadius: 8, | |
| fontFamily: | |
| "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", | |
| fontSize: 14, | |
| lineHeight: 1.5, | |
| }} | |
| /> | |
| {error ? ( | |
| <div style={{ color: "#b00020", fontSize: 14 }}>{error}</div> | |
| ) : null} | |
| <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}></div> | |
| <div style={{ color: "#666", fontSize: 12 }}> | |
| Press Enter to auto-capitalize the previous line. Your API key is stored | |
| locally in localStorage. | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment