Repository navigation
feat(remaining): deferred tasks — error pages, UX, telemetry, domain #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| "use client"; | ||
|
|
||
| /** | ||
| * Global Error Page — FASE-04 Error Handling | ||
| * | ||
| * Root-level error boundary for unrecoverable errors. | ||
| * This is the last resort — catches errors that the per-page | ||
| * error.js boundaries don't handle. | ||
| */ | ||
|
|
||
| export default function GlobalError({ error, reset }) { | ||
| return ( | ||
| <html> | ||
| <body | ||
| style={{ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| minHeight: "100vh", | ||
| padding: "24px", | ||
| background: "#0a0a0f", | ||
| color: "#e0e0e0", | ||
| fontFamily: "system-ui, -apple-system, sans-serif", | ||
| textAlign: "center", | ||
| margin: 0, | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| fontSize: "64px", | ||
| marginBottom: "16px", | ||
| }} | ||
| > | ||
| ⚠️ | ||
| </div> | ||
| <h1 | ||
| style={{ | ||
| fontSize: "28px", | ||
| fontWeight: 700, | ||
| marginBottom: "8px", | ||
| }} | ||
| > | ||
| Something went wrong | ||
| </h1> | ||
| <p | ||
| style={{ | ||
| fontSize: "15px", | ||
| color: "#888", | ||
| maxWidth: "400px", | ||
| lineHeight: 1.5, | ||
| marginBottom: "24px", | ||
| }} | ||
| > | ||
| An unexpected error occurred. This has been logged and our team will investigate. | ||
| </p> | ||
| {process.env.NODE_ENV === "development" && error?.message && ( | ||
| <pre | ||
| style={{ | ||
| padding: "16px", | ||
| borderRadius: "8px", | ||
| background: "rgba(239, 68, 68, 0.1)", | ||
| border: "1px solid rgba(239, 68, 68, 0.3)", | ||
| color: "#ef4444", | ||
| fontSize: "12px", | ||
| maxWidth: "600px", | ||
| overflow: "auto", | ||
| textAlign: "left", | ||
| marginBottom: "24px", | ||
| }} | ||
| > | ||
| {error.message} | ||
| </pre> | ||
| )} | ||
| <button | ||
| onClick={reset} | ||
| style={{ | ||
| padding: "12px 32px", | ||
| borderRadius: "10px", | ||
| background: "linear-gradient(135deg, #6366f1, #8b5cf6)", | ||
| color: "#fff", | ||
| border: "none", | ||
| fontSize: "14px", | ||
| fontWeight: 600, | ||
| cursor: "pointer", | ||
| transition: "transform 0.2s", | ||
| boxShadow: "0 4px 16px rgba(99, 102, 241, 0.3)", | ||
| }} | ||
| onMouseEnter={(e) => (e.target.style.transform = "translateY(-2px)")} | ||
| onMouseLeave={(e) => (e.target.style.transform = "translateY(0)")} | ||
| > | ||
| Try Again | ||
| </button> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||
| "use client"; | ||||
|
|
||||
|
Comment on lines
+1
to
+2
|
||||
| "use client"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'Go to Dashboard' link is styled like a button and includes a transition property, which suggests a hover effect is intended. However, no hover effect is currently implemented. For better user experience and consistency with other buttons in this PR, you should add one. A simple transform effect on hover would work well here.
<Link
href="/dashboard"
style={{
padding: "12px 32px",
borderRadius: "10px",
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
color: "#fff",
fontSize: "14px",
fontWeight: 600,
textDecoration: "none",
transition: "all 0.2s",
boxShadow: "0 4px 16px rgba(99, 102, 241, 0.3)",
display: "inline-block",
transform: "translateY(0)",
}}
onMouseEnter={(e) => (e.target.style.transform = "translateY(-2px)")}
onMouseLeave={(e) => (e.target.style.transform = "translateY(0)")}
>
Go to Dashboard
</Link>| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Combo Resolver — FASE-09 Domain Extraction (T-46) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Extracts combo resolution logic from handleChat into a dedicated | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * domain service. Handles model selection based on combo strategy | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * (priority, round-robin, random, least-used). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @module domain/comboResolver | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @typedef {import('./types.js').Combo} Combo | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Resolve which model to use from a combo based on its strategy. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {Combo} combo - The combo configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {{ modelUsageCounts?: Record<string, number> }} [context] - Optional context | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {{ model: string, index: number }} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @throws {Error} If combo has no models | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function resolveComboModel(combo, context = {}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const models = combo.models || []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (models.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Combo "${combo.name}" has no models configured`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Normalize models to { model, weight } format | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = models.map((m) => | ||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof m === "string" ? { model: m, weight: 1 } : m | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const strategy = combo.strategy || "priority"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (strategy) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case "priority": | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { model: normalized[0].model, index: 0 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| case "round-robin": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use a simple counter based on current time + combo id hash | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment here is misleading. It mentions using a 'combo id hash', but the implementation only uses
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const tick = Date.now() % normalized.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { model: normalized[tick].model, index: tick }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+44
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| case "random": { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Weighted random selection | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalWeight = normalized.reduce((sum, m) => sum + (m.weight || 1), 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let rand = Math.random() * totalWeight; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < normalized.length; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rand -= normalized[i].weight || 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+52
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalWeight = normalized.reduce((sum, m) => sum + (m.weight || 1), 0); | |
| let rand = Math.random() * totalWeight; | |
| for (let i = 0; i < normalized.length; i++) { | |
| rand -= normalized[i].weight || 1; | |
| const totalWeight = normalized.reduce((sum, m) => { | |
| const weight = m.weight ?? 1; | |
| return sum + (weight > 0 ? weight : 0); | |
| }, 0); | |
| let rand = Math.random() * totalWeight; | |
| for (let i = 0; i < normalized.length; i++) { | |
| const weight = normalized[i].weight ?? 1; | |
| if (weight <= 0) { | |
| continue; | |
| } | |
| rand -= weight; |
Copilot
AI
Feb 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The app already creates combos with strategy: "weighted" (and model entries containing { model, weight }). This resolver doesn’t handle the weighted strategy at all (it will fall through to default and always pick the first model), which will break weighted combos if/when this module is used. Add explicit weighted handling (and align supported strategy names with the rest of the codebase).
Copilot
AI
Feb 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getComboFallbacks doesn’t validate primaryIndex. If callers pass -1 or an index >= models.length, the returned fallback order becomes surprising (e.g., -1 returns the full list, >=len also returns full list). Consider normalizing primaryIndex into range (modulo) or throwing for invalid indices so fallback ordering is predictable.
| return [...models.slice(primaryIndex + 1), ...models.slice(0, primaryIndex)]; | |
| const modelCount = models.length; | |
| if (modelCount === 0) { | |
| // No models configured; no fallbacks available. | |
| return []; | |
| } | |
| // Ensure primaryIndex refers to a valid primary model so fallback ordering | |
| // is predictable (all models except the primary, in order). | |
| if ( | |
| !Number.isInteger(primaryIndex) || | |
| primaryIndex < 0 || | |
| primaryIndex >= modelCount | |
| ) { | |
| throw new RangeError( | |
| `Invalid primaryIndex ${primaryIndex} for combo "${combo.name}" with ${modelCount} models` | |
| ); | |
| } | |
| return [ | |
| ...models.slice(primaryIndex + 1), | |
| ...models.slice(0, primaryIndex), | |
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Directly manipulating
e.target.stylefor hover effects is not a recommended practice in React. It bypasses React's virtual DOM and can make component state harder to track. A more idiomatic approach is to use component state to manage dynamic styles.Since this pattern is used in a few new components in this PR (
Breadcrumbs.js,EmptyState.js), you might consider a broader refactor. Here's an example of how you could implement this using theuseStatehook:import { useState } from 'react';const [isHovered, setIsHovered] = useState(false);