← back to gyzhoe__assistant

Function bodies 138 total

All specs Real LLM only Function bodies
startObserver method · typescript · L38-L51 (14 LOC)
extension/src/content/sidebar-host.ts
  private startObserver(): void {
    this.observer = new MutationObserver(() => {
      // Debounce to avoid flooding on WHD's rapid partial DOM updates
      if (this.debounceTimer !== null) clearTimeout(this.debounceTimer)
      this.debounceTimer = setTimeout(() => {
        this.sendTicketData()
      }, OBSERVER_DEBOUNCE_MS)
    })

    this.observer.observe(document.body, {
      childList: true,
      subtree: true,
    })
  }
getBackendUrl function · typescript · L4-L11 (8 LOC)
extension/src/lib/api-client.ts
async function getBackendUrl(): Promise<string> {
  return new Promise((resolve) => {
    chrome.storage.sync.get(STORAGE_KEY_SETTINGS, (result) => {
      const settings = result[STORAGE_KEY_SETTINGS] as { backendUrl?: string } | undefined
      resolve(settings?.backendUrl ?? DEFAULT_BACKEND_URL)
    })
  })
}
getApiToken function · typescript · L14-L21 (8 LOC)
extension/src/lib/api-client.ts
async function getApiToken(): Promise<string> {
  return new Promise((resolve) => {
    chrome.storage.local.get(STORAGE_KEY_SECRETS, (result) => {
      const secrets = result[STORAGE_KEY_SECRETS] as { apiToken?: string } | undefined
      resolve(secrets?.apiToken ?? '')
    })
  })
}
buildHeaders function · typescript · L24-L29 (6 LOC)
extension/src/lib/api-client.ts
async function buildHeaders(extra: Record<string, string> = {}): Promise<Record<string, string>> {
  const token = await getApiToken()
  const headers: Record<string, string> = { 'Content-Type': 'application/json', ...extra }
  if (token) headers['X-Extension-Token'] = token
  return headers
}
sendNativeCommand function · typescript · L124-L134 (11 LOC)
extension/src/lib/api-client.ts
export function sendNativeCommand(action: string): Promise<NativeResponse> {
  return new Promise((resolve) => {
    chrome.runtime.sendNativeMessage(NATIVE_HOST, { action }, (response: NativeResponse) => {
      if (chrome.runtime.lastError) {
        resolve({ ok: false, error: chrome.runtime.lastError.message })
      } else {
        resolve(response)
      }
    })
  })
}
ApiError class · typescript · L136-L143 (8 LOC)
extension/src/lib/api-client.ts
export class ApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly body: unknown
  ) {
    super(`API error ${status}`)
  }
}
constructor method · typescript · L137-L142 (6 LOC)
extension/src/lib/api-client.ts
  constructor(
    public readonly status: number,
    public readonly body: unknown
  ) {
    super(`API error ${status}`)
  }
Same scanner, your repo: https://repobility.com — Repobility
OptionsApp function · typescript · L5-L13 (9 LOC)
extension/src/options/OptionsApp.tsx
export default function OptionsApp(): React.ReactElement {
  const { resolvedTheme } = useTheme()

  return (
    <div className="options-shell" data-theme={resolvedTheme}>
      <OptionsPage />
    </div>
  )
}
OptionsPage function · typescript · L16-L229 (214 LOC)
extension/src/options/OptionsPage.tsx
export default function OptionsPage(): React.ReactElement {
  const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS)
  const [apiToken, setApiToken] = useState('')
  const [isSaving, setIsSaving] = useState(false)
  const [saveMsg, setSaveMsg] = useState('')
  const [models, setModels] = useState<string[]>([])
  const [selectorsExpanded, setSelectorsExpanded] = useState(false)

  useEffect(() => {
    storage.getSettings().then(setSettings)
    apiClient.models().then(setModels).catch(() => {})
    chrome.storage.local.get(STORAGE_KEY_SECRETS, (result) => {
      const secrets = result[STORAGE_KEY_SECRETS] as { apiToken?: string } | undefined
      setApiToken(secrets?.apiToken ?? '')
    })
  }, [])

  const handleChange = (field: keyof AppSettings, value: string) => {
    setSettings((prev) => ({ ...prev, [field]: value }))
  }

  const handleSelectorChange = (field: keyof SelectorConfig, value: string) => {
    setSettings((prev) => {
      const overrides = { ...pr
debugLog function · typescript · L85-L89 (5 LOC)
extension/src/shared/constants.ts
export function debugLog(...args: unknown[]): void {
  if (import.meta.env.DEV) {
    console.log('[AI-HD]', ...args)
  }
}
debugError function · typescript · L92-L96 (5 LOC)
extension/src/shared/constants.ts
export function debugError(...args: unknown[]): void {
  if (import.meta.env.DEV) {
    console.error('[AI-HD]', ...args)
  }
}
App function · typescript · L9-L42 (34 LOC)
extension/src/sidebar/App.tsx
export default function App(): React.ReactElement {
  const mainRef = useRef<HTMLElement>(null)
  const { resolvedTheme, themeSetting, cycleTheme } = useTheme()

  useEffect(() => {
    mainRef.current?.focus()
  }, [])

  return (
    <div className="app-shell" data-theme={resolvedTheme}>
      <ErrorBoundary>
        <header className="app-header" role="banner">
          <div className="brand-mark" aria-hidden="true">
            AI
          </div>
          <div className="brand-copy">
            <h1>Helpdesk Assistant</h1>
            <p>AI-powered ticket responses</p>
          </div>
          <ThemeToggle
            theme={themeSetting}
            resolvedTheme={resolvedTheme}
            onCycle={cycleTheme}
          />
        </header>
        <main ref={mainRef} className="app-main" role="main" tabIndex={-1}>
          <BackendControl />
          <KnowledgePanel />
          <ReplyPanel />
        </main>
      </ErrorBoundary>
    </div>
  )
}
BackendControl function · typescript · L10-L224 (215 LOC)
extension/src/sidebar/components/BackendControl.tsx
export function BackendControl(): React.ReactElement {
  const [status, setStatus] = useState<BackendStatus>('checking')
  const [ollamaOk, setOllamaOk] = useState(false)
  const [ollamaAction, setOllamaAction] = useState<'idle' | 'starting' | 'stopping'>('idle')
  const [version, setVersion] = useState('')
  const [nativeError, setNativeError] = useState('')
  const [collapsed, setCollapsed] = useState(false)
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const mountedRef = useRef(true)

  const ticketData = useSidebarStore((s) => s.ticketData)
  const isTicketPage = useSidebarStore((s) => s.isTicketPage)
  const selectedModel = useSidebarStore((s) => s.selectedModel)

  const clearTimer = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current)
      timerRef.current = null
    }
  }

  const checkHealth = useCallback(async () => {
    try {
      const h = await apiClient.health()
      if (!mountedRef.current) return
      setStatus('online'
ErrorBoundary class · typescript · L12-L51 (40 LOC)
extension/src/sidebar/components/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  private handleCopyError = () => {
    if (this.state.error) {
      navigator.clipboard.writeText(this.state.error.stack ?? this.state.error.message).catch(() => {})
    }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="p-4 flex flex-col gap-3 error-fallback" role="alert">
          <p className="title">Something went wrong</p>
          <p className="detail">
            {this.state.error?.message ?? 'An unexpected error occurred.'}
          </p>
          <p className="hint">
            Try refreshing the page. If the issue persists, copy the error and report it.
          </p>
          <button
            onClick={this.handleCopyError}
            className="link-btn"
       
constructor method · typescript · L13-L16 (4 LOC)
extension/src/sidebar/components/ErrorBoundary.tsx
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
getDerivedStateFromError method · typescript · L18-L20 (3 LOC)
extension/src/sidebar/components/ErrorBoundary.tsx
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }
render method · typescript · L28-L50 (23 LOC)
extension/src/sidebar/components/ErrorBoundary.tsx
  render() {
    if (this.state.hasError) {
      return (
        <div className="p-4 flex flex-col gap-3 error-fallback" role="alert">
          <p className="title">Something went wrong</p>
          <p className="detail">
            {this.state.error?.message ?? 'An unexpected error occurred.'}
          </p>
          <p className="hint">
            Try refreshing the page. If the issue persists, copy the error and report it.
          </p>
          <button
            onClick={this.handleCopyError}
            className="link-btn"
            aria-label="Copy error details to clipboard"
          >
            Copy error details
          </button>
        </div>
      )
    }
    return this.props.children
  }
ErrorState function · typescript · L8-L28 (21 LOC)
extension/src/sidebar/components/ErrorState.tsx
export function ErrorState({ message, onRetry }: ErrorStateProps): React.ReactElement {
  return (
    <div
      className="alert-banner error"
      role="alert"
      aria-live="assertive"
    >
      <p className="alert-title">Error</p>
      <p className="alert-message">{message}</p>
      {onRetry && (
        <button
          onClick={onRetry}
          className="link-btn"
          aria-label="Retry generating reply"
        >
          Try again
        </button>
      )}
    </div>
  )
}
ImportTab function · typescript · L6-L177 (172 LOC)
extension/src/sidebar/components/ImportTab.tsx
export function ImportTab(): React.ReactElement {
  const {
    stagedFiles, uploadStatus, progress, errorMessage, results,
    addFiles, removeFile, clearFiles, startUpload, cancelUpload, resetState,
  } = useKnowledgeImport()

  const fileInputRef = useRef<HTMLInputElement>(null)
  const [dragCount, setDragCount] = useState(0)

  const isDragOver = dragCount > 0
  const isUploading = uploadStatus === 'uploading'

  const handleDragEnter = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setDragCount((c) => c + 1)
  }, [])

  const handleDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setDragCount((c) => c - 1)
  }, [])

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault()
  }, [])

  const handleDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault()
    setDragCount(0)
    addFiles([...e.dataTransfer.files])
  }, [addFiles])

  const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInpu
InsertButton function · typescript · L8-L81 (74 LOC)
extension/src/sidebar/components/InsertButton.tsx
export function InsertButton(): React.ReactElement {
  const reply = useSidebarStore((s) => s.reply)
  const isGenerating = useSidebarStore((s) => s.isGenerating)
  const setIsInserted = useSidebarStore((s) => s.setIsInserted)
  const [insertState, setInsertState] = useState<InsertState>('idle')
  const [errorMsg, setErrorMsg] = useState('')
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  useEffect(() => {
    const listener = (message: ContentToSidebarMessage) => {
      if (message.type === 'INSERT_SUCCESS') {
        setInsertState('success')
        setIsInserted(true)
        if (timerRef.current) clearTimeout(timerRef.current)
        timerRef.current = setTimeout(() => setInsertState('idle'), 2000)
      } else if (message.type === 'INSERT_FAILED') {
        setInsertState('error')
        setErrorMsg(message.payload.reason)
        if (timerRef.current) clearTimeout(timerRef.current)
        timerRef.current = setTimeout(() => setInsertState('idle'), 30
KnowledgePanel function · typescript · L8-L109 (102 LOC)
extension/src/sidebar/components/KnowledgePanel.tsx
export function KnowledgePanel(): React.ReactElement {
  const [collapsed, setCollapsed] = useState(true)
  const [activeTab, setActiveTab] = useState<'import' | 'manage'>('import')
  const [docCounts, setDocCounts] = useState<Record<string, number>>({})
  const mountedRef = useRef(true)
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const totalDocs = Object.values(docCounts).reduce((sum, n) => sum + n, 0)

  const clearTimer = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current)
      timerRef.current = null
    }
  }, [])

  const fetchCounts = useCallback(async () => {
    try {
      const h = await apiClient.health()
      if (mountedRef.current) setDocCounts(h.chroma_doc_counts)
    } catch {
      // Ignore — backend may be offline
    }
  }, [])

  const schedulePoll = useCallback(() => {
    clearTimer()
    timerRef.current = setTimeout(() => {
      if (mountedRef.current) {
        fetchCounts().finally(() => {
     
formatCollectionName function · typescript · L14-L16 (3 LOC)
extension/src/sidebar/components/ManageTab.tsx
function formatCollectionName(name: string): string {
  return COLLECTION_LABELS[name] ?? name
}
ManageTab function · typescript · L18-L76 (59 LOC)
extension/src/sidebar/components/ManageTab.tsx
export function ManageTab({ docCounts, onRefresh }: ManageTabProps): React.ReactElement {
  const [confirmingClear, setConfirmingClear] = useState<string | null>(null)
  const [clearingCollection, setClearingCollection] = useState<string | null>(null)

  const collections = Object.entries(docCounts).filter(([, count]) => count > 0)

  const handleClear = useCallback(async (name: string) => {
    setClearingCollection(name)
    setConfirmingClear(null)
    try {
      await apiClient.clearCollection(name)
      onRefresh()
    } finally {
      setClearingCollection(null)
    }
  }, [onRefresh])

  if (collections.length === 0) {
    return <p className="support-text">No documents imported yet</p>
  }

  return (
    <div className="kb-collection-list">
      {collections.map(([name, count]) => (
        <div key={name} className="kb-collection-row">
          <span className="kb-file-name">{formatCollectionName(name)}</span>
          <span className="kb-file-size">{count} docs</span>
If a scraper extracted this row, it came from Repobility (https://repobility.com)
ModelSelector function · typescript · L6-L34 (29 LOC)
extension/src/sidebar/components/ModelSelector.tsx
export function ModelSelector(): React.ReactElement {
  const selectedModel = useSidebarStore((s) => s.selectedModel)
  const setSelectedModel = useSidebarStore((s) => s.setSelectedModel)
  const [models, setModels] = useState<string[]>([DEFAULT_MODEL])

  useEffect(() => {
    apiClient.models().then((list) => {
      if (list.length > 0) setModels(list)
    }).catch(() => {
      // Keep default if models endpoint unavailable
    })
  }, [])

  return (
    <div className="control-row">
      <label htmlFor="model-select">Model</label>
      <select
        id="model-select"
        value={selectedModel}
        onChange={(e) => setSelectedModel(e.target.value)}
        aria-label="Select LLM model"
      >
        {models.map((m) => (
          <option key={m} value={m}>{m}</option>
        ))}
      </select>
    </div>
  )
}
ReplyPanel function · typescript · L11-L110 (100 LOC)
extension/src/sidebar/components/ReplyPanel.tsx
export function ReplyPanel(): React.ReactElement {
  useTicketData()
  const { generate } = useGenerateReply()

  const ticketData = useSidebarStore((s) => s.ticketData)
  const isTicketPage = useSidebarStore((s) => s.isTicketPage)
  const reply = useSidebarStore((s) => s.reply)
  const isGenerating = useSidebarStore((s) => s.isGenerating)
  const generateError = useSidebarStore((s) => s.generateError)
  const isInserted = useSidebarStore((s) => s.isInserted)
  const cancelGeneration = useSidebarStore((s) => s.cancelGeneration)
  const isEditingReply = useSidebarStore((s) => s.isEditingReply)
  const setIsEditingReply = useSidebarStore((s) => s.setIsEditingReply)
  const setReply = useSidebarStore((s) => s.setReply)

  return (
    <>
      {/* Ticket context */}
      <section className="panel" aria-label="Ticket details">
        <h2 className="section-heading">Ticket context</h2>
        {!isTicketPage && (
          <p className="support-text">Open a WHD ticket page to begin. This 
SkeletonLoader function · typescript · L3-L19 (17 LOC)
extension/src/sidebar/components/SkeletonLoader.tsx
export function SkeletonLoader(): React.ReactElement {
  return (
    <div
      className="flex flex-col gap-2 p-4"
      role="status"
      aria-label="Generating reply, please wait"
      aria-live="polite"
    >
      <div className="skeleton h-3 rounded w-full" />
      <div className="skeleton h-3 rounded w-4/5" />
      <div className="skeleton h-3 rounded w-3/4" />
      <div className="skeleton h-3 rounded w-full mt-2" />
      <div className="skeleton h-3 rounded w-5/6" />
      <span className="sr-only">Generating reply…</span>
    </div>
  )
}
ThemeToggle function · typescript · L33-L47 (15 LOC)
extension/src/sidebar/components/ThemeToggle.tsx
export function ThemeToggle({ theme, resolvedTheme, onCycle }: ThemeToggleProps): React.ReactElement {
  const Icon = ICON_MAP[theme]

  return (
    <button
      type="button"
      className="theme-toggle"
      onClick={onCycle}
      aria-label={`Switch theme, current: ${theme}`}
      title={`Theme: ${theme} (${resolvedTheme})`}
    >
      <Icon />
    </button>
  )
}
TicketContext function · typescript · L8-L21 (14 LOC)
extension/src/sidebar/components/TicketContext.tsx
export function TicketContext({ ticket }: TicketContextProps): React.ReactElement {
  return (
    <div className="context-card">
      <p className="subject" title={ticket.subject}>
        {ticket.subject || <span className="text-empty">No subject</span>}
      </p>
      <div className="meta-row">
        {ticket.category && <span>{ticket.category}</span>}
        {ticket.status && <span>{ticket.status}</span>}
        {ticket.requesterName && <span>{ticket.requesterName}</span>}
      </div>
    </div>
  )
}
useGenerateReply function · typescript · L6-L56 (51 LOC)
extension/src/sidebar/hooks/useGenerateReply.ts
export function useGenerateReply() {
  const ticketData = useSidebarStore((s) => s.ticketData)
  const selectedModel = useSidebarStore((s) => s.selectedModel)
  const setReply = useSidebarStore((s) => s.setReply)
  const setIsGenerating = useSidebarStore((s) => s.setIsGenerating)
  const setGenerateError = useSidebarStore((s) => s.setGenerateError)
  const setLastResponse = useSidebarStore((s) => s.setLastResponse)
  const setIsInserted = useSidebarStore((s) => s.setIsInserted)
  const setAbortController = useSidebarStore((s) => s.setAbortController)
  const setIsEditingReply = useSidebarStore((s) => s.setIsEditingReply)
  const { settings } = useSettings()

  const generate = useCallback(async () => {
    if (!ticketData) return

    const ctrl = new AbortController()
    setAbortController(ctrl)
    setIsGenerating(true)
    setGenerateError(null)
    setReply('')
    setIsInserted(false)
    setIsEditingReply(false)

    try {
      const response = await apiClient.generate({
      
formatFileSize function · typescript · L27-L31 (5 LOC)
extension/src/sidebar/hooks/useKnowledgeImport.ts
function formatFileSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
getExtension function · typescript · L33-L36 (4 LOC)
extension/src/sidebar/hooks/useKnowledgeImport.ts
function getExtension(name: string): string {
  const idx = name.lastIndexOf('.')
  return idx >= 0 ? name.slice(idx).toLowerCase() : ''
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
useKnowledgeImport function · typescript · L38-L204 (167 LOC)
extension/src/sidebar/hooks/useKnowledgeImport.ts
export function useKnowledgeImport() {
  const [stagedFiles, setStagedFiles] = useState<StagedFile[]>([])
  const [uploadStatus, setUploadStatus] = useState<UploadStatus>('idle')
  const [progress, setProgress] = useState<UploadProgress | null>(null)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
  const [results, setResults] = useState<IngestUploadResponse[]>([])

  const abortRef = useRef<AbortController | null>(null)
  const dismissRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const clearDismissTimer = useCallback(() => {
    if (dismissRef.current) {
      clearTimeout(dismissRef.current)
      dismissRef.current = null
    }
  }, [])

  const resetState = useCallback(() => {
    clearDismissTimer()
    setStagedFiles([])
    setUploadStatus('idle')
    setProgress(null)
    setErrorMessage(null)
    setResults([])
  }, [clearDismissTimer])

  const addFiles = useCallback((files: File[]) => {
    setErrorMessage(null)

    const invalid = file
useSettings function · typescript · L5-L22 (18 LOC)
extension/src/sidebar/hooks/useSettings.ts
export function useSettings() {
  const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    storage.getSettings().then((s) => {
      setSettings(s)
      setIsLoading(false)
    })
  }, [])

  const updateSettings = async (updates: Partial<AppSettings>) => {
    await storage.saveSettings(updates)
    setSettings((prev) => ({ ...prev, ...updates }))
  }

  return { settings, isLoading, updateSettings }
}
resolveTheme function · typescript · L15-L18 (4 LOC)
extension/src/sidebar/hooks/useTheme.ts
function resolveTheme(setting: AppSettings['theme'], osDark: boolean): ResolvedTheme {
  if (setting === 'system') return osDark ? 'dark' : 'light'
  return setting
}
useTheme function · typescript · L20-L43 (24 LOC)
extension/src/sidebar/hooks/useTheme.ts
export function useTheme(): UseThemeReturn {
  const { settings, updateSettings } = useSettings()
  const [resolved, setResolved] = useState<ResolvedTheme>(() =>
    resolveTheme(settings.theme, window.matchMedia(MEDIA_QUERY).matches)
  )

  useEffect(() => {
    const mq = window.matchMedia(MEDIA_QUERY)
    const apply = () => setResolved(resolveTheme(settings.theme, mq.matches))

    apply()
    mq.addEventListener('change', apply)
    return () => mq.removeEventListener('change', apply)
  }, [settings.theme])

  const cycleTheme = useCallback(() => {
    const next: AppSettings['theme'] =
      settings.theme === 'system' ? 'light' :
      settings.theme === 'light' ? 'dark' : 'system'
    void updateSettings({ theme: next })
  }, [settings.theme, updateSettings])

  return { resolvedTheme: resolved, themeSetting: settings.theme, cycleTheme }
}
useTicketData function · typescript · L5-L26 (22 LOC)
extension/src/sidebar/hooks/useTicketData.ts
export function useTicketData(): void {
  const setTicketData = useSidebarStore((s) => s.setTicketData)
  const setIsTicketPage = useSidebarStore((s) => s.setIsTicketPage)

  useEffect(() => {
    // Request current ticket data on mount
    chrome.runtime.sendMessage({ type: 'REQUEST_TICKET_DATA' }).catch(() => {})

    const listener = (message: ContentToSidebarMessage) => {
      if (message.type === 'TICKET_DATA_UPDATED') {
        setTicketData(message.payload)
        setIsTicketPage(true)
      } else if (message.type === 'NOT_A_TICKET_PAGE') {
        setTicketData(null)
        setIsTicketPage(false)
      }
    }

    chrome.runtime.onMessage.addListener(listener)
    return () => chrome.runtime.onMessage.removeListener(listener)
  }, [setTicketData, setIsTicketPage])
}
draw_icon function · python · L13-L122 (110 LOC)
scripts/generate-icons.py
def draw_icon(size: int) -> Image.Image:
    """Draw a headset icon with optional sparkle at the given size."""
    # Work at 4x for anti-aliasing, then downsample
    scale = 4
    s = size * scale
    img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)

    # Rounded background
    margin = int(s * 0.06)
    radius = int(s * 0.22)
    draw.rounded_rectangle(
        [margin, margin, s - margin, s - margin],
        radius=radius,
        fill=ACCENT,
    )

    cx, cy = s // 2, s // 2

    if size <= 16:
        # Simplified icon at 16px: just headband arc + ear cups
        band_w = max(int(s * 0.07), 2)
        band_r = int(s * 0.28)
        band_box = [cx - band_r, cy - band_r - int(s * 0.05),
                     cx + band_r, cy + band_r - int(s * 0.05)]
        draw.arc(band_box, start=200, end=340, fill=WHITE, width=band_w)

        # Ear cups — rounded rectangles
        cup_w = int(s * 0.16)
        cup_h = int(s * 0.24)
        cup_r = int(s * 0.05)
main function · python · L125-L131 (7 LOC)
scripts/generate-icons.py
def main() -> None:
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    for size in (16, 48, 128):
        icon = draw_icon(size)
        out_path = OUTPUT_DIR / f"icon{size}.png"
        icon.save(out_path, "PNG")
        print(f"Generated {out_path} ({size}x{size})")
‹ prevpage 3 / 3