From 655bba98602110ea6a7ecb5872546f8f51b4d98a Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 20 Jan 2026 13:47:07 -0600 Subject: [PATCH 01/20] first draft of a health system --- .../contentIndexes/indexes/sqliteIndex.ts | 56 +++++++++-- .../codexCellEditorProvider.ts | 49 ++++++++-- .../codexCellEditorProvider/codexDocument.ts | 76 ++++++++++++++- .../utils/cellUtils.ts | 1 + .../navigationWebviewProvider.ts | 18 ++++ .../translationSuggestions/llmCompletion.ts | 13 ++- types/index.d.ts | 2 + .../CodexCellEditor/CellContentDisplay.tsx | 19 ++-- .../src/CodexCellEditor/CodexCellEditor.tsx | 32 +++++++ .../src/CodexCellEditor/HealthIndicator.tsx | 93 +++++++++++++++++++ .../src/NavigationView/index.tsx | 48 +++++++++- 11 files changed, 377 insertions(+), 30 deletions(-) create mode 100644 webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx diff --git a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts index 5600f9bfd..73ecf1088 100644 --- a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts +++ b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts @@ -15,7 +15,7 @@ const debug = (message: string, ...args: any[]) => { }; // Schema version for migrations -export const CURRENT_SCHEMA_VERSION = 12; // Added milestone_index column for O(1) milestone lookup +export const CURRENT_SCHEMA_VERSION = 12; // Added milestone_index and t_health columns export class SQLiteIndexManager { private sql: SqlJsStatic | null = null; @@ -302,10 +302,13 @@ export class SQLiteIndexManager { t_audio_validation_count INTEGER DEFAULT 0, t_audio_validated_by TEXT, t_audio_is_fully_validated BOOLEAN DEFAULT FALSE, - + -- Milestone index for O(1) lookup (0-based milestone index, NULL if no milestone) milestone_index INTEGER, - + + -- Health score for translation quality (0.0-1.0, default 0.3 for unverified) + t_health REAL DEFAULT 0.3, + FOREIGN KEY (s_file_id) REFERENCES files(id) ON DELETE SET NULL, FOREIGN KEY (t_file_id) REFERENCES files(id) ON DELETE SET NULL ) @@ -866,7 +869,7 @@ export class SQLiteIndexManager { // Add target-specific metadata columns if (cellType === 'target') { columns.push('t_current_edit_timestamp', 't_validation_count', 't_validated_by', 't_is_fully_validated', - 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated'); + 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated', 't_health'); values.push( actualEditTimestamp, // Only t_current_edit_timestamp for target cells (no redundant t_updated_at) extractedMetadata.validationCount || 0, @@ -874,7 +877,8 @@ export class SQLiteIndexManager { extractedMetadata.isFullyValidated ? 1 : 0, extractedMetadata.audioValidationCount || 0, extractedMetadata.audioValidatedBy || null, - extractedMetadata.audioIsFullyValidated ? 1 : 0 + extractedMetadata.audioIsFullyValidated ? 1 : 0, + extractedMetadata.health ?? 0.3 ); } @@ -1034,7 +1038,7 @@ export class SQLiteIndexManager { // Add target-specific metadata columns if (cellType === 'target') { columns.push('t_current_edit_timestamp', 't_validation_count', 't_validated_by', 't_is_fully_validated', - 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated'); + 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated', 't_health'); values.push( actualEditTimestamp, // Only t_current_edit_timestamp for target cells (no redundant t_updated_at) extractedMetadata.validationCount || 0, @@ -1042,7 +1046,8 @@ export class SQLiteIndexManager { extractedMetadata.isFullyValidated ? 1 : 0, extractedMetadata.audioValidationCount || 0, extractedMetadata.audioValidatedBy || null, - extractedMetadata.audioIsFullyValidated ? 1 : 0 + extractedMetadata.audioIsFullyValidated ? 1 : 0, + extractedMetadata.health ?? 0.3 ); } @@ -1596,6 +1601,33 @@ export class SQLiteIndexManager { return null; } + // Get health values for multiple cells + async getCellsHealth(cellIds: string[]): Promise> { + if (!this.db || cellIds.length === 0) return new Map(); + + const placeholders = cellIds.map(() => '?').join(','); + const stmt = this.db.prepare(` + SELECT cell_id, t_health + FROM cells + WHERE cell_id IN (${placeholders}) + `); + + const result = new Map(); + try { + stmt.bind(cellIds); + while (stmt.step()) { + const row = stmt.getAsObject() as { cell_id: string; t_health: number | null }; + const health = row.t_health ?? 0.3; + result.set(row.cell_id, health); + console.log(`[SQLiteIndex] getCellsHealth: ${row.cell_id} -> ${health} (raw: ${row.t_health})`); + } + } finally { + stmt.free(); + } + console.log(`[SQLiteIndex] getCellsHealth: queried ${cellIds.length} cells, found ${result.size}`); + return result; + } + // Get translation pair by cell ID async getTranslationPair(cellId: string): Promise { if (!this.db) return null; @@ -3662,6 +3694,7 @@ export class SQLiteIndexManager { audioValidationCount?: number; audioValidatedBy?: string; audioIsFullyValidated?: boolean; + health?: number; } { const result: { currentEditTimestamp?: number | null; @@ -3671,6 +3704,7 @@ export class SQLiteIndexManager { audioValidationCount?: number; audioValidatedBy?: string; audioIsFullyValidated?: boolean; + health?: number; } = {}; if (!metadata || typeof metadata !== "object" || cellType !== "target") { @@ -3725,6 +3759,14 @@ export class SQLiteIndexManager { result.currentEditTimestamp = audioDetails.latestTimestamp; } + // Extract health from metadata (if set) + if (typeof metadata.health === 'number') { + result.health = metadata.health; + console.log(`[SQLiteIndex] Extracted health ${metadata.health} from metadata`); + } else { + console.log(`[SQLiteIndex] No health in metadata, keys:`, Object.keys(metadata || {})); + } + return result; } diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index 70e83ab26..d53ec2fdc 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -46,6 +46,7 @@ import { isMatchingFilePair as isMatchingFilePairUtil, } from "../../utils/fileTypeUtils"; import { getCorrespondingSourceUri } from "../../utils/codexNotebookUtils"; +import { getSQLiteIndexManager } from "../../activationHelpers/contextAware/contentIndexes/indexes/sqliteIndexManager"; // Enable debug logging if needed const DEBUG_MODE = false; @@ -1084,6 +1085,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0 && e.edits[0].type === "audioValidation") { const selectedAudioId = document.getExplicitAudioSelection(e.edits[0].cellId) ?? undefined; // Broadcast the audio validation update to all webviews for this document @@ -1105,6 +1108,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0) { + const indexManager = getSQLiteIndexManager(); + if (indexManager) { + const healthMap = await indexManager.getCellsHealth(exampleCellIds); + console.log(`[HealthCalc] Health map size: ${healthMap.size}, values:`, Array.from(healthMap.entries())); + if (healthMap.size > 0) { + const healthValues = Array.from(healthMap.values()); + const avgHealth = healthValues.reduce((a, b) => a + b, 0) / healthValues.length; + // Ensure health is at least 0.3 (baseline) - otherwise low-health examples + // would produce even lower health for new cells + calculatedHealth = Math.max(0.3, 0.9 * avgHealth); + console.log(`[HealthCalc] avgHealth: ${avgHealth}, calculatedHealth: ${calculatedHealth}`); + } + } else { + console.log(`[HealthCalc] No index manager available`); + } + } else { + console.log(`[HealthCalc] No example cell IDs provided`); + } + // If multiple variants are present, send to the webview for selection if (completionResult && Array.isArray((completionResult as any).variants) && (completionResult as any).variants.length > 1) { const { variants, testId, testName, names } = completionResult as any; @@ -3410,7 +3441,10 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider this.isValidValidationEntry(entry) && !entry.isDeleted + ); + if (activeValidations.length === 0) { + // No validations remain, reset health to baseline + cellToUpdate.metadata.health = 0.3; + } + } + // Mark document as dirty this._isDirty = true; @@ -2218,12 +2255,14 @@ export class CodexCellDocument implements vscode.CustomDocument { cellId, type: "validation", validatedBy: latestEdit.validatedBy, + health: cellToUpdate.metadata.health, }), edits: [ { cellId, type: "validation", validatedBy: latestEdit.validatedBy, + health: cellToUpdate.metadata.health, }, ], }); @@ -2234,10 +2273,16 @@ export class CodexCellDocument implements vscode.CustomDocument { username, validationCount: latestEdit.validatedBy.filter(entry => this.isValidValidationEntry(entry) && !entry.isDeleted).length, cellHasContent: !!(cellToUpdate.value && cellToUpdate.value.trim()), - editsCount: cellToUpdate.metadata.edits.length + editsCount: cellToUpdate.metadata.edits.length, + healthAfterValidation: cellToUpdate.metadata.health }); - // Database update will happen automatically when document is saved + // Immediately sync health to SQLite so example queries get up-to-date values + if (cellToUpdate.value) { + Promise.resolve(this.addCellToIndexImmediately(cellId, cellToUpdate.value, EditType.USER_EDIT)).catch(error => { + console.error(`[CodexDocument] Failed to sync health to SQLite for cell ${cellId}:`, error); + }); + } } // Method to validate a cell's audio by a user @@ -2318,6 +2363,21 @@ export class CodexCellDocument implements vscode.CustomDocument { } cellToUpdate.metadata.attachments[attachmentId] = attachment; + // Update health based on validation state + if (validate) { + // Set health to 100% when audio is validated (audio validation = full confidence) + cellToUpdate.metadata.health = 1.0; + } else { + // When un-validating, check if any active validations remain + const activeValidations = attachment.validatedBy.filter( + (entry: any) => this.isValidValidationEntry(entry) && !entry.isDeleted + ); + if (activeValidations.length === 0) { + // No audio validations remain, reset health to baseline + cellToUpdate.metadata.health = 0.3; + } + } + // Mark document as dirty this._isDirty = true; @@ -2327,17 +2387,24 @@ export class CodexCellDocument implements vscode.CustomDocument { cellId, type: "audioValidation", validatedBy: attachment.validatedBy, + health: cellToUpdate.metadata.health, }), edits: [ { cellId, type: "audioValidation", validatedBy: attachment.validatedBy, + health: cellToUpdate.metadata.health, }, ], }); - // Database update will happen automatically when document is saved + // Immediately sync health to SQLite so example queries get up-to-date values + if (cellToUpdate.value) { + Promise.resolve(this.addCellToIndexImmediately(cellId, cellToUpdate.value, EditType.USER_EDIT)).catch(error => { + console.error(`[CodexDocument] Failed to sync health to SQLite for cell ${cellId}:`, error); + }); + } } /** @@ -3232,6 +3299,7 @@ export class CodexCellDocument implements vscode.CustomDocument { selectionTimestamp: cell.metadata?.selectionTimestamp, type: cell.metadata?.type || null, lastUpdated: Date.now(), + health: cell.metadata?.health, }; // Check if this cell has text validation data for logging diff --git a/src/providers/codexCellEditorProvider/utils/cellUtils.ts b/src/providers/codexCellEditorProvider/utils/cellUtils.ts index 8e0ac14be..728aeacb8 100644 --- a/src/providers/codexCellEditorProvider/utils/cellUtils.ts +++ b/src/providers/codexCellEditorProvider/utils/cellUtils.ts @@ -76,6 +76,7 @@ export function convertCellToQuillContent(cell: CustomNotebookCellData): QuillCe selectionTimestamp: cell.metadata?.selectionTimestamp, parentId: cell.metadata?.parentId, isLocked: cell.metadata?.isLocked, + health: cell.metadata?.health, }, }; } diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 26b90f72a..7bfb18111 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -553,6 +553,14 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const textValidationLevels = computeLevelPercents(textValidationCounts, minimumValidationsRequired); const audioValidationLevels = computeLevelPercents(audioValidationCounts, minimumAudioValidationsRequired); + // Compute average health from cells + const healthValues = unmergedCells + .map((cell) => cell.metadata?.health) + .filter((h): h is number => typeof h === 'number'); + const averageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : 0.3; // Default to baseline if no health data + const { percentTranslationsCompleted, percentAudioTranslationsCompleted, @@ -589,6 +597,7 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { audioValidationLevels, requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth, }, sortOrder, fileDisplayName: metadata?.fileDisplayName, @@ -665,6 +674,14 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const averageTextValidationLevels = avgArray('textValidationLevels', textLen); const averageAudioValidationLevels = avgArray('audioValidationLevels', audioLen); + // Compute average health for the corpus group + const healthValues = itemsInGroup + .map((item) => item.progress?.averageHealth) + .filter((h): h is number => typeof h === 'number'); + const groupAverageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : 0.3; + const sortedItems = itemsInGroup.sort((a, b) => { if (a.sortOrder && b.sortOrder) { return a.sortOrder.localeCompare(b.sortOrder); @@ -689,6 +706,7 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { audioValidationLevels: averageAudioValidationLevels, requiredTextValidations: vscode.workspace.getConfiguration("codex-project-manager").get("validationCount", 1) || 1, requiredAudioValidations: vscode.workspace.getConfiguration("codex-project-manager").get("validationCountAudio", 1) || 1, + averageHealth: groupAverageHealth, }, }); }); diff --git a/src/providers/translationSuggestions/llmCompletion.ts b/src/providers/translationSuggestions/llmCompletion.ts index 94ec77232..5badd87e1 100644 --- a/src/providers/translationSuggestions/llmCompletion.ts +++ b/src/providers/translationSuggestions/llmCompletion.ts @@ -59,7 +59,8 @@ function handleABTestResult( currentCellId: string, testIdPrefix: string, completionConfig: CompletionConfig, - returnHTML: boolean + returnHTML: boolean, + exampleCellIds?: string[] ): LLMCompletionResult | null { if (result && Array.isArray(result.variants) && result.variants.length === 2) { const allowHtml = Boolean(completionConfig.allowHtmlPredictions); @@ -70,6 +71,7 @@ function handleABTestResult( testId: `${currentCellId}-${testIdPrefix}-${Date.now()}`, testName: result.testName, names: result.names, + exampleCellIds, }; } return null; @@ -81,6 +83,7 @@ export interface LLMCompletionResult { testId?: string; testName?: string; names?: string[]; + exampleCellIds?: string[]; // IDs of cells used as few-shot examples } export async function llmCompletion( @@ -160,6 +163,10 @@ export async function llmCompletion( numberOfFewShotExamples, completionConfig.useOnlyValidatedExamples ); + + // Extract example cell IDs for health calculation + const exampleCellIds = finalExamples.map(ex => ex.cellId); + if (completionConfig.debugMode) { console.debug(`[llmCompletion] Retrieved ${finalExamples.length} few-shot examples:`, finalExamples.map(ex => ({ cellId: ex.cellId, source: ex.sourceCell?.content?.substring(0, 50) + '...', target: ex.targetCell?.content?.substring(0, 50) + '...' }))); } @@ -294,7 +301,8 @@ export async function llmCompletion( currentCellId, "countAB", completionConfig, - returnHTML + returnHTML, + exampleCellIds ); if (testResult) { @@ -328,6 +336,7 @@ export async function llmCompletion( return { variants, isABTest: false, // Identical variants – UI should hide A/B controls + exampleCellIds, }; } catch (error) { // Check if this is a cancellation error and re-throw as-is diff --git a/types/index.d.ts b/types/index.d.ts index b179b86dd..b05d40804 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -930,6 +930,7 @@ type CustomCellMetaData = BaseCustomCellMetaData & { cellLabel?: string; selectedAudioId?: string; // Points to attachment key for explicit audio selection selectionTimestamp?: number; // Timestamp when selectedAudioId was last set + health?: number; // Cell health score 0.0-1.0 (0.3 for new/unverified cells, calculated from examples for LLM-generated) }; export type CustomNotebookCellData = Omit & { @@ -1763,6 +1764,7 @@ interface CodexItem { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; // Average health score 0-1 for all cells in the file }; sortOrder?: string; isProjectDictionary?: boolean; diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 901fb3f59..f8c23609c 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -21,6 +21,7 @@ import { CELL_DISPLAY_MODES } from "./CodexCellEditor"; // Import the cell displ import "./TranslationAnimations.css"; // Import the animation CSS import { useTooltip } from "./contextProviders/TooltipContext"; import CommentsBadge from "./CommentsBadge"; +import HealthIndicator from "./HealthIndicator"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import ReactMarkdown from "react-markdown"; import { @@ -1140,8 +1141,9 @@ const CellContentDisplay: React.FC = React.memo( {lineNumber} )} - {/* Audio Validation Button - show for non-source text only */} - {!isSourceText && SHOW_VALIDATION_BUTTON && ( + {/* Audio Validation Button - show only when audio exists */} + {!isSourceText && SHOW_VALIDATION_BUTTON && + audioState !== "none" && audioState !== "deletedOnly" && (
= React.memo( currentUsername={currentUsername} requiredAudioValidations={requiredAudioValidations} setShowSparkleButton={setShowSparkleButton} - disabled={ - isInTranslationProcess || - audioState === "none" || - audioState === "deletedOnly" - } + disabled={isInTranslationProcess} disabledReason={ isInTranslationProcess ? "Translation in progress" - : audioState === "none" || - audioState === "deletedOnly" - ? "Audio validation requires audio" : undefined } /> @@ -1292,6 +1287,10 @@ const CellContentDisplay: React.FC = React.memo( )}
{getAlertDot()} + {/* Health Indicator - positioned below the inline action buttons */} + {!isSourceText && ( + + )} )} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 90d11a226..96bc854ac 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -1203,6 +1203,22 @@ const CodexCellEditor: React.FC = () => { // Listen for individual validation state updates to refresh progress if (message.type === "providerUpdatesValidationState") { + // Update cell health in translationUnits when validation includes health + if (message.content?.cellId && message.content?.health !== undefined) { + setTranslationUnits((prevUnits) => + prevUnits.map((unit) => + unit.cellMarkers[0] === message.content.cellId + ? { + ...unit, + metadata: { + ...unit.metadata, + health: message.content.health, + }, + } + : unit + ) + ); + } // Refresh progress for current milestone after text validation completes const milestoneIdx = currentMilestoneIndexRef.current; if (milestoneIndex && milestoneIdx < milestoneIndex.milestones.length) { @@ -1212,6 +1228,22 @@ const CodexCellEditor: React.FC = () => { // Listen for audio validation state updates to refresh progress if (message.type === "providerUpdatesAudioValidationState") { + // Update cell health in translationUnits when audio validation includes health + if (message.content?.cellId && message.content?.health !== undefined) { + setTranslationUnits((prevUnits) => + prevUnits.map((unit) => + unit.cellMarkers[0] === message.content.cellId + ? { + ...unit, + metadata: { + ...unit.metadata, + health: message.content.health, + }, + } + : unit + ) + ); + } // Refresh progress for current milestone after audio validation completes const milestoneIdx = currentMilestoneIndexRef.current; if (milestoneIndex && milestoneIdx < milestoneIndex.milestones.length) { diff --git a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx new file mode 100644 index 000000000..6454e3b4e --- /dev/null +++ b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx @@ -0,0 +1,93 @@ +import React, { useState } from "react"; + +interface HealthIndicatorProps { + health: number | undefined; + className?: string; +} + +/** + * Displays a small health indicator bar showing the translation quality score. + * - Red (0-30%): Low confidence / unverified + * - Yellow (30-70%): Medium confidence + * - Green (70-100%): High confidence / verified + */ +const HealthIndicator: React.FC = ({ health, className = "" }) => { + const [isHovered, setIsHovered] = useState(false); + + // Don't render if health is undefined + if (health === undefined) { + return null; + } + + // Clamp health to 0-1 range + const normalizedHealth = Math.max(0, Math.min(1, health)); + const percentage = Math.round(normalizedHealth * 100); + + // Determine color based on health level + const getColor = (h: number): string => { + if (h < 0.3) return "#ef4444"; // red-500 + if (h < 0.7) return "#eab308"; // yellow-500 + return "#22c55e"; // green-500 + }; + + const getBackgroundColor = (h: number): string => { + if (h < 0.3) return "rgba(239, 68, 68, 0.2)"; // red with opacity + if (h < 0.7) return "rgba(234, 179, 8, 0.2)"; // yellow with opacity + return "rgba(34, 197, 94, 0.2)"; // green with opacity + }; + + const getLabel = (h: number): string => { + if (h >= 1.0) return "Validated"; + if (h >= 0.7) return "High confidence"; + if (h >= 0.3) return "Medium confidence"; + return "Unverified"; + }; + + const color = getColor(normalizedHealth); + const backgroundColor = getBackgroundColor(normalizedHealth); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+
+
+ + {percentage}% + +
+ ); +}; + +export default HealthIndicator; diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index 05e082d5a..de2c360ae 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -7,7 +7,7 @@ import { Progress } from "../components/ui/progress"; import "../tailwind.css"; import { CodexItem } from "types"; import { Popover, PopoverContent, PopoverTrigger } from "../components/ui/popover"; -import { Languages } from "lucide-react"; +import { Languages, Heart } from "lucide-react"; import { RenameModal } from "../components/RenameModal"; // Declare the acquireVsCodeApi function @@ -611,6 +611,7 @@ function NavigationView() { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; } ) => { if (typeof progress !== "object") return null; @@ -645,8 +646,53 @@ function NavigationView() { const requiredText = progress.requiredTextValidations; const requiredAudio = progress.requiredAudioValidations; + // Health indicator - compute color and percentage + const health = progress.averageHealth ?? 0.3; + const healthPercent = Math.round(health * 100); + const getHealthColor = (h: number) => { + if (h < 0.3) return "#ef4444"; // red + if (h < 0.7) return "#eab308"; // yellow + return "#22c55e"; // green + }; + const healthColor = getHealthColor(health); + return (
+ {/* Health indicator - compact pill at the top */} +
+ +
+
+
+
+ + {healthPercent}% + +
+
+
From 06602e39a9933b4162e954eb747ba31b4892d42b Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 20 Jan 2026 15:05:35 -0600 Subject: [PATCH 02/20] better UI and calculation --- .../codexCellEditorProvider/codexDocument.ts | 11 +++- .../navigationWebviewProvider.ts | 10 +-- .../CodexCellEditor/CellContentDisplay.tsx | 5 +- .../src/NavigationView/index.tsx | 62 ++++++++++--------- 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index f81b7af1d..33fe6dafb 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -317,11 +317,16 @@ export class CodexCellDocument implements vscode.CustomDocument { } // Set health on cell metadata if provided (for LLM generations) - if (health !== undefined) { + // Only assign health when the cell has actual content - empty cells should not have health scores + const hasContent = newContent && newContent.trim().length > 0 && newContent !== ""; + if (health !== undefined && hasContent) { cellToUpdate.metadata.health = health; - } else if (cellToUpdate.metadata.health === undefined) { - // Initialize with base health if not set + } else if (cellToUpdate.metadata.health === undefined && hasContent) { + // Initialize with base health if not set and cell has content cellToUpdate.metadata.health = 0.3; + } else if (!hasContent) { + // Clear health if cell becomes empty + cellToUpdate.metadata.health = undefined; } // For user edits, only add the edit if content has actually changed diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 7bfb18111..03a07d84e 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -553,13 +553,15 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const textValidationLevels = computeLevelPercents(textValidationCounts, minimumValidationsRequired); const audioValidationLevels = computeLevelPercents(audioValidationCounts, minimumAudioValidationsRequired); - // Compute average health from cells + // Compute average health from cells - only include cells that have content + // Empty cells should not have health scores and should not count towards average const healthValues = unmergedCells + .filter((cell) => cell.value && cell.value.trim().length > 0 && cell.value !== "") .map((cell) => cell.metadata?.health) .filter((h): h is number => typeof h === 'number'); const averageHealth = healthValues.length > 0 ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length - : 0.3; // Default to baseline if no health data + : undefined; // No health data for files with only empty cells const { percentTranslationsCompleted, @@ -674,13 +676,13 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const averageTextValidationLevels = avgArray('textValidationLevels', textLen); const averageAudioValidationLevels = avgArray('audioValidationLevels', audioLen); - // Compute average health for the corpus group + // Compute average health for the corpus group - only from items that have health data const healthValues = itemsInGroup .map((item) => item.progress?.averageHealth) .filter((h): h is number => typeof h === 'number'); const groupAverageHealth = healthValues.length > 0 ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length - : 0.3; + : undefined; // No health data if no items have health const sortedItems = itemsInGroup.sort((a, b) => { if (a.sortOrder && b.sortOrder) { diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index f8c23609c..33214d8e4 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -1288,8 +1288,9 @@ const CellContentDisplay: React.FC = React.memo(
{getAlertDot()} {/* Health Indicator - positioned below the inline action buttons */} - {!isSourceText && ( - + {/* Only show health indicator for non-empty cells */} + {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( + )}
)} diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index de2c360ae..3e308621b 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -647,51 +647,55 @@ function NavigationView() { const requiredAudio = progress.requiredAudioValidations; // Health indicator - compute color and percentage - const health = progress.averageHealth ?? 0.3; - const healthPercent = Math.round(health * 100); + // Only show health indicator when there's actual health data (not for empty files) + const health = progress.averageHealth; + const hasHealthData = typeof health === 'number'; + const healthPercent = hasHealthData ? Math.round(health * 100) : 0; const getHealthColor = (h: number) => { if (h < 0.3) return "#ef4444"; // red if (h < 0.7) return "#eab308"; // yellow return "#22c55e"; // green }; - const healthColor = getHealthColor(health); + const healthColor = hasHealthData ? getHealthColor(health) : "#888"; return (
- {/* Health indicator - compact pill at the top */} -
- -
+ {/* Health indicator - compact pill at the top, only shown when health data exists */} + {hasHealthData && ( +
+
+ > +
+
+ + {healthPercent}% +
- - {healthPercent}% -
-
+ )}
From 5bcb0df2fe737a7c1911843340920e0d16add05b Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 27 Jan 2026 14:17:48 -0600 Subject: [PATCH 03/20] feature flag, improved UI --- package.json | 5 ++ .../codexCellEditorProvider.ts | 6 ++ .../codexCellEditorProvider/codexDocument.ts | 21 +++++ .../navigationWebviewProvider.ts | 6 ++ types/index.d.ts | 1 + .../CodexCellEditor/CellContentDisplay.tsx | 6 +- .../src/CodexCellEditor/CellList.tsx | 4 + .../ChapterNavigationHeader.tsx | 23 ++++- .../src/CodexCellEditor/CodexCellEditor.tsx | 6 ++ .../src/CodexCellEditor/HealthIndicator.tsx | 7 +- .../components/MilestoneAccordion.tsx | 88 +++++++++++++------ .../hooks/useVSCodeMessageHandler.ts | 6 ++ .../src/NavigationView/index.tsx | 72 +++++++-------- webviews/codex-webviews/src/lib/types.ts | 1 + 14 files changed, 179 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 2f8f5dafa..fb8babed7 100644 --- a/package.json +++ b/package.json @@ -520,6 +520,11 @@ "default": "", "description": "Name of the project" }, + "codex-project-manager.showHealthIndicators": { + "type": "boolean", + "default": false, + "description": "Show health indicators for translation cells and files" + }, "codex-project-manager.validationCount": { "type": "integer", "default": 1, diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index d53ec2fdc..09d9ff6e2 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -851,6 +851,11 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider("showHealthIndicators", false); + this.postMessageToWebview(webviewPanel, { type: "providerSendsInitialContentPaginated", rev, @@ -865,6 +870,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { const progress: Record = {}; const cells = this._documentData.cells || []; @@ -1715,6 +1721,7 @@ export class CodexCellDocument implements vscode.CustomDocument { audioValidationLevels: [], requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth: undefined, }; continue; } @@ -1773,12 +1780,26 @@ export class CodexCellDocument implements vscode.CustomDocument { fullyValidatedCells ); + // Calculate average health for cells with content + const healthValues = subsectionCells + .filter((cell) => + cell.cellContent && + cell.cellContent.trim().length > 0 && + cell.cellContent !== "" + ) + .map((cell) => cell.metadata?.health) + .filter((h): h is number => typeof h === 'number'); + const averageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : undefined; + progress[subsectionIdx] = { ...progressPercentages, textValidationLevels, audioValidationLevels, requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth, }; } diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 03a07d84e..d1289b783 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -830,10 +830,16 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { this.serializeItem(item) ); + // Get feature flag for health indicators + const showHealthIndicators = vscode.workspace + .getConfiguration("codex-project-manager") + .get("showHealthIndicators", false); + safePostMessageToView(this._view, { command: "updateItems", codexItems: serializedCodexItems, dictionaryItems: serializedDictItems, + showHealthIndicators, }); if (this.bibleBookMap) { diff --git a/types/index.d.ts b/types/index.d.ts index b05d40804..c8da99f84 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1811,6 +1811,7 @@ type EditorReceiveMessages = validationCountAudio?: number; isAuthenticated?: boolean; userAccessLevel?: number; + showHealthIndicators?: boolean; } | { type: "providerSendsCellPage"; diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 33214d8e4..9b6ec329b 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -76,6 +76,7 @@ interface CellContentDisplayProps { isAudioOnly?: boolean; showInlineBacktranslations?: boolean; backtranslation?: any; + showHealthIndicators?: boolean; } const DEBUG_ENABLED = false; @@ -472,6 +473,7 @@ const CellContentDisplay: React.FC = React.memo( isAudioOnly = false, showInlineBacktranslations = false, backtranslation, + showHealthIndicators = false, }) => { // const { cellContent, timestamps, editHistory } = cell; // I don't think we use this const cellIds = cell.cellMarkers; @@ -1288,9 +1290,9 @@ const CellContentDisplay: React.FC = React.memo(
{getAlertDot()} {/* Health Indicator - positioned below the inline action buttons */} - {/* Only show health indicator for non-empty cells */} + {/* Only show health indicator for non-empty cells when feature is enabled */} {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( - + )}
)} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx index 6cf54048f..dedf6a736 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx @@ -73,6 +73,7 @@ export interface CellListProps { currentMilestoneIndex?: number; currentSubsectionIndex?: number; cellsPerPage?: number; + showHealthIndicators?: boolean; } const DEBUG_ENABLED = false; @@ -125,6 +126,7 @@ const CellList: React.FC = ({ currentMilestoneIndex = 0, currentSubsectionIndex = 0, cellsPerPage = 50, + showHealthIndicators = false, }) => { const numberOfEmptyCellsToRender = 1; const { unsavedChanges, toggleFlashingBorder } = useContext(UnsavedChangesContext); @@ -810,6 +812,7 @@ const CellList: React.FC = ({ isAudioOnly={isAudioOnly} showInlineBacktranslations={showInlineBacktranslations} backtranslation={backtranslationsMap.get(cellMarkers[0])} + showHealthIndicators={showHealthIndicators} /> ); @@ -995,6 +998,7 @@ const CellList: React.FC = ({ isAudioOnly={isAudioOnly} showInlineBacktranslations={showInlineBacktranslations} backtranslation={backtranslationsMap.get(cellMarkers[0])} + showHealthIndicators={showHealthIndicators} /> ); diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index 2ea655e11..27855bb90 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -92,6 +92,7 @@ interface ChapterNavigationHeaderProps { subsectionProgress?: Record; allSubsectionProgress?: Record>; requestSubsectionProgress?: (milestoneIdx: number) => void; + showHealthIndicators?: boolean; } export function ChapterNavigationHeader({ @@ -153,6 +154,7 @@ export function ChapterNavigationHeader({ subsectionProgress, allSubsectionProgress, requestSubsectionProgress, + showHealthIndicators = false, }: // Removed onToggleCorrectionEditor since it will be a VS Code command now ChapterNavigationHeaderProps) { const [showConfirm, setShowConfirm] = useState(false); @@ -1254,7 +1256,26 @@ ChapterNavigationHeaderProps) { anchorRef={chapterTitleRef} calculateSubsectionProgress={calculateSubsectionProgress} requestSubsectionProgress={requestSubsectionProgress} - vscode={vscode} + handleEditMilestoneModalOpen={handleEditMilestoneModalOpen} + showHealthIndicators={showHealthIndicators} + /> + +
); diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 96bc854ac..cd099620c 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -196,6 +196,9 @@ const CodexCellEditor: React.FC = () => { ); const [backtranslationsMap, setBacktranslationsMap] = useState>(new Map()); + // Health indicator display state + const [showHealthIndicators, setShowHealthIndicators] = useState(false); + // Simplified state - now we just mirror the provider's state const [autocompletionState, setAutocompletionState] = useState<{ isProcessing: boolean; @@ -1437,6 +1440,7 @@ const CodexCellEditor: React.FC = () => { setChapterNumber(chapter); }, setAudioAttachments: setAudioAttachments, + setShowHealthIndicators: setShowHealthIndicators, showABTestVariants: (data) => { const { variants, cellId, testId, testName, names, abProbability } = data as any; const count = Array.isArray(variants) ? variants.length : 0; @@ -2978,6 +2982,7 @@ const CodexCellEditor: React.FC = () => { subsectionProgress={subsectionProgress[currentMilestoneIndex]} allSubsectionProgress={subsectionProgress} requestSubsectionProgress={requestSubsectionProgressForMilestone} + showHealthIndicators={showHealthIndicators} />
@@ -3060,6 +3065,7 @@ const CodexCellEditor: React.FC = () => { currentMilestoneIndex={currentMilestoneIndex} currentSubsectionIndex={currentSubsectionIndex} cellsPerPage={cellsPerPage} + showHealthIndicators={showHealthIndicators} />
diff --git a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx index 6454e3b4e..ccd2ac1c4 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; interface HealthIndicatorProps { health: number | undefined; className?: string; + show?: boolean; } /** @@ -11,11 +12,11 @@ interface HealthIndicatorProps { * - Yellow (30-70%): Medium confidence * - Green (70-100%): High confidence / verified */ -const HealthIndicator: React.FC = ({ health, className = "" }) => { +const HealthIndicator: React.FC = ({ health, className = "", show = true }) => { const [isHovered, setIsHovered] = useState(false); - // Don't render if health is undefined - if (health === undefined) { + // Don't render if health is undefined or show is false + if (health === undefined || !show) { return null; } diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx index 10042ceaa..4cc83ebdc 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx @@ -10,7 +10,7 @@ import { import { ProgressDots } from "./ProgressDots"; import { deriveSubsectionPercentages, getProgressDisplay } from "../utils/progressUtils"; import MicrophoneIcon from "../../components/ui/icons/MicrophoneIcon"; -import { Languages, Check, RotateCcw } from "lucide-react"; +import { Languages, Heart } from "lucide-react"; import type { Subsection, ProgressPercentages } from "../../lib/types"; import type { MilestoneIndex, MilestoneInfo } from "../../../../../types"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; @@ -43,9 +43,11 @@ interface MilestoneAccordionProps { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; }; requestSubsectionProgress?: (milestoneIdx: number) => void; - vscode: any; + handleEditMilestoneModalOpen: () => void; + showHealthIndicators?: boolean; } export function MilestoneAccordion({ @@ -62,7 +64,8 @@ export function MilestoneAccordion({ anchorRef, calculateSubsectionProgress, requestSubsectionProgress, - vscode, + handleEditMilestoneModalOpen, + showHealthIndicators = false, }: MilestoneAccordionProps) { // Layout constants const DROPDOWN_MAX_HEIGHT_VIEWPORT_PERCENT = 60; // 60vh @@ -304,6 +307,7 @@ export function MilestoneAccordion({ audioValidationLevels: backendProgress.audioValidationLevels, requiredTextValidations: backendProgress.requiredTextValidations, requiredAudioValidations: backendProgress.requiredAudioValidations, + averageHealth: backendProgress.averageHealth, }; } @@ -325,6 +329,7 @@ export function MilestoneAccordion({ audioValidationLevels: undefined, requiredTextValidations: undefined, requiredAudioValidations: undefined, + averageHealth: undefined, }; }; @@ -869,29 +874,60 @@ export function MilestoneAccordion({ }`} > {subsection.label} - +
+ {showHealthIndicators && typeof progress.averageHealth === 'number' && ( +
+ = 0.7 + ? "var(--vscode-charts-green, #22c55e)" + : progress.averageHealth >= 0.3 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + /> + = 0.7 + ? "var(--vscode-charts-green, #22c55e)" + : progress.averageHealth >= 0.3 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + > + {Math.round(progress.averageHealth * 100)}% + +
+ )} + +
); })} diff --git a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts index d55860f12..4bfe69658 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts +++ b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts @@ -58,6 +58,7 @@ interface UseVSCodeMessageHandlerProps { singleCellTranslationFailed?: () => void; setChapterNumber?: (chapterNumber: number) => void; setAudioAttachments: Dispatch>; + setShowHealthIndicators?: Dispatch>; // A/B testing handlers showABTestVariants?: (data: { variants: string[]; cellId: string; testId: string; }) => void; @@ -109,6 +110,7 @@ export const useVSCodeMessageHandler = ({ singleCellTranslationFailed, setChapterNumber, setAudioAttachments, + setShowHealthIndicators, showABTestVariants, setContentPaginated, handleCellPage, @@ -331,6 +333,10 @@ export const useVSCodeMessageHandler = ({ message.sourceCellMap ); } + // Update health indicators setting if provided + if (setShowHealthIndicators && typeof message.showHealthIndicators === 'boolean') { + setShowHealthIndicators(message.showHealthIndicators); + } // Bootstrap audio availability from initial cells try { const units = (message.cells || []) as QuillCellContent[]; diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index 3e308621b..5515c867b 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -33,6 +33,7 @@ interface State { searchQuery: string; bibleBookMap: Map | undefined; hasReceivedInitialData: boolean; + showHealthIndicators: boolean; renameModal: { isOpen: boolean; item: CodexItem | null; @@ -176,8 +177,8 @@ function NavigationView() { previousExpandedGroups: null, searchQuery: "", bibleBookMap: undefined, - hasReceivedInitialData: false, + showHealthIndicators: false, renameModal: { isOpen: false, item: null, @@ -274,6 +275,7 @@ function NavigationView() { codexItems: processedCodexItems, dictionaryItems: message.dictionaryItems || [], hasReceivedInitialData: true, + showHealthIndicators: message.showHealthIndicators ?? false, }; }); break; @@ -646,53 +648,41 @@ function NavigationView() { const requiredText = progress.requiredTextValidations; const requiredAudio = progress.requiredAudioValidations; - // Health indicator - compute color and percentage - // Only show health indicator when there's actual health data (not for empty files) + // Health indicator - compute percentage + // Only show health indicator when there's actual health data and feature flag is enabled const health = progress.averageHealth; - const hasHealthData = typeof health === 'number'; + const hasHealthData = typeof health === 'number' && state.showHealthIndicators; const healthPercent = hasHealthData ? Math.round(health * 100) : 0; - const getHealthColor = (h: number) => { - if (h < 0.3) return "#ef4444"; // red - if (h < 0.7) return "#eab308"; // yellow - return "#22c55e"; // green - }; - const healthColor = hasHealthData ? getHealthColor(health) : "#888"; return (
- {/* Health indicator - compact pill at the top, only shown when health data exists */} + {/* Health indicator - styled to match other progress bars */} {hasHealthData && ( -
- -
-
-
+
+ + + +
+
+
+
= 70 + ? "var(--vscode-charts-green, #22c55e)" + : healthPercent >= 30 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + /> +
+
+ + {healthPercent}% + +
- - {healthPercent}% -
)} diff --git a/webviews/codex-webviews/src/lib/types.ts b/webviews/codex-webviews/src/lib/types.ts index 145319027..6f495db35 100644 --- a/webviews/codex-webviews/src/lib/types.ts +++ b/webviews/codex-webviews/src/lib/types.ts @@ -49,6 +49,7 @@ export interface ProgressPercentages { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; } export interface Subsection { From fe10f1b7af5b25d4746032a93f7aa50dfaa9e735 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Fri, 30 Jan 2026 21:47:40 -0700 Subject: [PATCH 04/20] fix: complete merge of health system with main branch features Restore missing code from conflict resolution: - Add vscode prop back to MilestoneAccordion interface and component - Add RenameModal import and milestone editing handlers to ChapterNavigationHeader - Import Check and RotateCcw icons alongside Heart Co-Authored-By: Claude Opus 4.5 --- .../ChapterNavigationHeader.tsx | 43 +++++++++++++++++++ .../components/MilestoneAccordion.tsx | 4 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index 27855bb90..bb1e0a379 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -25,6 +25,7 @@ import { } from "../components/ui/dropdown-menu"; import { Slider } from "../components/ui/slider"; import { Alert, AlertDescription } from "../components/ui/alert"; +import { RenameModal } from "../components/RenameModal"; interface ChapterNavigationHeaderProps { chapterNumber: number; @@ -162,6 +163,8 @@ ChapterNavigationHeaderProps) { const [autoDownloadAudioOnOpen, setAutoDownloadAudioOnOpenState] = useState(false); const [showMilestoneAccordion, setShowMilestoneAccordion] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [milestoneNewName, setMilestoneNewName] = useState(""); + const [showEditMilestoneModal, setShowEditMilestoneModal] = useState(false); const chapterTitleRef = useRef(null); const headerContainerRef = useRef(null); const [truncatedBookName, setTruncatedBookName] = useState(null); @@ -538,6 +541,45 @@ ChapterNavigationHeaderProps) { } }; + const handleEditMilestoneModalOpen = () => { + const currentMilestone = milestoneIndex?.milestones[currentMilestoneIndex]; + if (currentMilestone) { + setMilestoneNewName(currentMilestone.value); + setShowEditMilestoneModal(true); + } + }; + + const handleEditMilestoneModalClose = () => { + setShowEditMilestoneModal(false); + setMilestoneNewName(""); + }; + + const handleEditMilestoneModalConfirm = () => { + const currentMilestone = milestoneIndex?.milestones[currentMilestoneIndex]; + if ( + currentMilestone && + milestoneNewName.trim() !== "" && + milestoneNewName.trim() !== currentMilestone.value + ) { + // Send message to update milestone value + vscode.postMessage({ + command: "updateMilestoneValue", + content: { + milestoneIndex: currentMilestoneIndex, + newValue: milestoneNewName.trim(), + }, + }); + } + handleEditMilestoneModalClose(); + }; + + // Close accordion when rename modal opens + useEffect(() => { + if (showEditMilestoneModal) { + setShowMilestoneAccordion(false); + } + }, [showEditMilestoneModal]); + const handleFontSizeChange = (value: number[]) => { const newFontSize = value[0]; setFontSize(newFontSize); @@ -1256,6 +1298,7 @@ ChapterNavigationHeaderProps) { anchorRef={chapterTitleRef} calculateSubsectionProgress={calculateSubsectionProgress} requestSubsectionProgress={requestSubsectionProgress} + vscode={vscode} handleEditMilestoneModalOpen={handleEditMilestoneModalOpen} showHealthIndicators={showHealthIndicators} /> diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx index 4cc83ebdc..bd830543e 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx @@ -10,7 +10,7 @@ import { import { ProgressDots } from "./ProgressDots"; import { deriveSubsectionPercentages, getProgressDisplay } from "../utils/progressUtils"; import MicrophoneIcon from "../../components/ui/icons/MicrophoneIcon"; -import { Languages, Heart } from "lucide-react"; +import { Languages, Heart, Check, RotateCcw } from "lucide-react"; import type { Subsection, ProgressPercentages } from "../../lib/types"; import type { MilestoneIndex, MilestoneInfo } from "../../../../../types"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; @@ -46,6 +46,7 @@ interface MilestoneAccordionProps { averageHealth?: number; }; requestSubsectionProgress?: (milestoneIdx: number) => void; + vscode: any; handleEditMilestoneModalOpen: () => void; showHealthIndicators?: boolean; } @@ -64,6 +65,7 @@ export function MilestoneAccordion({ anchorRef, calculateSubsectionProgress, requestSubsectionProgress, + vscode, handleEditMilestoneModalOpen, showHealthIndicators = false, }: MilestoneAccordionProps) { From 69372eea5e7ca980fec2219e0ba945bbe5b35fb5 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 01:41:13 -0700 Subject: [PATCH 05/20] feat: enhance audio validation with health indicators - Introduced health score and radial progress for audio validation status. - Added tooltip support for health information display. - Updated ValidationButton and ValidationStatusIcon components to utilize health data. - Cleaned up CellContentDisplay by removing the HealthIndicator component and integrating health props directly. This update improves user feedback during validation processes and enhances the overall UI experience. --- .../AudioValidationStatusIcon.tsx | 267 +++++++++++++++++- .../CodexCellEditor/CellContentDisplay.tsx | 51 ++-- .../src/CodexCellEditor/ValidationButton.tsx | 5 + 3 files changed, 285 insertions(+), 38 deletions(-) diff --git a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx index b6d241a6f..f942c3f5f 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; +import { useTooltip } from "./contextProviders/TooltipContext"; export interface ValidationStatusIconProps { isValidationInProgress: boolean; @@ -7,8 +8,18 @@ export interface ValidationStatusIconProps { requiredValidations: number; isValidatedByCurrentUser: boolean; displayValidationText?: boolean; + health?: number; // Health score (0-1) for radial progress when unverified + showHealthRadial?: boolean; // Whether to show radial health progress (only for text validation) + isPendingValidation?: boolean; // Whether validation is pending (for animation) } +// Helper function to get health color for radial progress +const getHealthColor = (health: number): string => { + if (health < 0.3) return "#ef4444"; // red-500 + if (health < 0.7) return "#eab308"; // yellow-500 + return "#22c55e"; // green-500 +}; + const ValidationStatusIcon: React.FC = ({ isValidationInProgress, isDisabled, @@ -16,7 +27,141 @@ const ValidationStatusIcon: React.FC = ({ requiredValidations, isValidatedByCurrentUser, displayValidationText, + health, + showHealthRadial = false, // Default to false - only show for text validation + isPendingValidation = false, }) => { + // Only use tooltip if we have health data and radial progress is enabled + const shouldUseTooltip = + showHealthRadial && health !== undefined && health !== null && currentValidations === 0; + const { showTooltip, hideTooltip } = useTooltip(); + const [isHovered, setIsHovered] = useState(false); + const [animatedHealth, setAnimatedHealth] = useState(null); + const tooltipTimeoutRef = useRef(null); + const radialProgressRef = useRef(null); + const animationFrameRef = useRef(null); + const animationStartTimeRef = useRef(null); + const animationStartHealthRef = useRef(0); + + // Handle validation animation: animate health from current value to 1.0 + useEffect(() => { + // When validation starts (pending), animate health to 100% + // Continue animation even if validation completes (isValidatedByCurrentUser becomes true) + if ( + isPendingValidation && + showHealthRadial && + health !== undefined && + health !== null && + animatedHealth === null + ) { + const startHealth = Math.max(0, Math.min(1, health)); + animationStartHealthRef.current = startHealth; + animationStartTimeRef.current = Date.now(); + setAnimatedHealth(startHealth); + + const animate = () => { + if (animationStartTimeRef.current === null) return; + + const elapsed = Date.now() - animationStartTimeRef.current; + const duration = 400; // 400ms animation + const progress = Math.min(elapsed / duration, 1); + + // Ease-out cubic for smooth animation + const eased = 1 - Math.pow(1 - progress, 3); + const newHealth = + animationStartHealthRef.current + (1 - animationStartHealthRef.current) * eased; + + setAnimatedHealth(newHealth); + + if (progress < 1) { + animationFrameRef.current = requestAnimationFrame(animate); + } else { + // Animation complete - set to 1.0 and clear after a brief moment to allow transition to checkmark + setAnimatedHealth(1.0); + setTimeout(() => { + setAnimatedHealth(null); + }, 150); + } + }; + + animationFrameRef.current = requestAnimationFrame(animate); + } else if (!isPendingValidation && !isValidatedByCurrentUser && animatedHealth !== null) { + // Reset animation if validation is cancelled (pending becomes false but not validated) + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + animationStartTimeRef.current = null; + setAnimatedHealth(null); + } + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [isPendingValidation, isValidatedByCurrentUser, showHealthRadial, health, animatedHealth]); + + // Handle tooltip with 750ms delay when hovering over radial progress + useEffect(() => { + // Only show tooltip for unverified cells with health data and radial progress enabled + if (!shouldUseTooltip) { + return; + } + + const shouldShowTooltip = isHovered; + + if (shouldShowTooltip) { + tooltipTimeoutRef.current = window.setTimeout(() => { + // Double-check conditions before showing tooltip + if (!radialProgressRef.current || !isHovered) { + return; + } + + const normalizedHealth = Math.max(0, Math.min(1, health!)); + const healthPercentage = Math.round(normalizedHealth * 100); + const getHealthLabel = (h: number): string => { + if (h >= 1.0) return "Validated"; + if (h >= 0.7) return "High confidence"; + if (h >= 0.3) return "Medium confidence"; + return "Unverified"; + }; + const label = getHealthLabel(normalizedHealth); + + // Get element position for tooltip + const rect = radialProgressRef.current.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top - 5; // Position slightly above the icon + + showTooltip( +
+ {healthPercentage}% - {label} +
, + x, + y + ); + }, 750); + } else { + // Clear timeout and hide tooltip when not hovering + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current); + tooltipTimeoutRef.current = null; + } + if (!isHovered) { + hideTooltip(); + } + } + + return () => { + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current); + tooltipTimeoutRef.current = null; + } + if (!isHovered) { + hideTooltip(); + } + }; + }, [isHovered, shouldUseTooltip, health, showTooltip, hideTooltip]); if (isValidationInProgress) { return ( = ({ ); } - if (currentValidations === 0) { + // Show radial progress only when unverified (currentValidations === 0) OR during validation animation + // Once validated (currentValidations > 0) and animation complete, show checkmark instead + const isUnverified = currentValidations === 0; + const showRadialDuringAnimation = animatedHealth !== null; // Show during entire animation including when it reaches 1.0 + + if (isUnverified || showRadialDuringAnimation) { + // Show radial progress when unverified, health is available, and showHealthRadial is true (text validation only) + // Use animated health if validation is in progress, otherwise use actual health + const effectiveHealth = animatedHealth !== null ? animatedHealth : health; + const showRadialProgress = + showHealthRadial && + effectiveHealth !== undefined && + effectiveHealth !== null && + (isUnverified || showRadialDuringAnimation); + const normalizedHealth = showRadialProgress ? Math.max(0, Math.min(1, effectiveHealth)) : 0; + const healthPercentage = showRadialProgress ? Math.round(normalizedHealth * 100) : 0; + const healthColor = showRadialProgress ? getHealthColor(normalizedHealth) : undefined; + + // SVG circle parameters for radial progress + // Use slightly larger size to accommodate the radial progress ring + const iconSize = 12; // Original icon size + const containerSize = 18; // Container size to fit radial progress + const strokeWidth = 2.5; + const radius = (containerSize - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const offset = circumference - normalizedHealth * circumference; + return ( -
- +
+ {showRadialProgress ? ( +
{ + e.stopPropagation(); + setIsHovered(true); + }} + onMouseLeave={(e) => { + e.stopPropagation(); + // Small delay to prevent parent handlers from interfering + setTimeout(() => { + setIsHovered(false); + }, 10); + }} + onMouseMove={(e) => { + // Keep tooltip alive while mouse is moving over the element + e.stopPropagation(); + }} + > + {/* Radial progress circle */} + + {/* Background circle */} + + {/* Progress circle */} + + + {/* Icon in center */} + +
+ ) : ( + + )} {displayValidationText && No validators}
); diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 9b6ec329b..49c47d751 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -21,7 +21,6 @@ import { CELL_DISPLAY_MODES } from "./CodexCellEditor"; // Import the cell displ import "./TranslationAnimations.css"; // Import the animation CSS import { useTooltip } from "./contextProviders/TooltipContext"; import CommentsBadge from "./CommentsBadge"; -import HealthIndicator from "./HealthIndicator"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import ReactMarkdown from "react-markdown"; import { @@ -1144,26 +1143,30 @@ const CellContentDisplay: React.FC = React.memo(
)} {/* Audio Validation Button - show only when audio exists */} - {!isSourceText && SHOW_VALIDATION_BUTTON && - audioState !== "none" && audioState !== "deletedOnly" && ( -
- -
- )} + {!isSourceText && + SHOW_VALIDATION_BUTTON && + audioState !== "none" && + audioState !== "deletedOnly" && ( +
+ +
+ )} {/* Audio Play Button - show for both source and non-source text */} {audioAttachments && audioAttachments[cellIds[0]] !== undefined && @@ -1226,6 +1229,7 @@ const CellContentDisplay: React.FC = React.memo( ? "Validation disabled: no text" : undefined; })()} + health={cell.metadata?.health} />
)} @@ -1289,11 +1293,6 @@ const CellContentDisplay: React.FC = React.memo( )}
{getAlertDot()} - {/* Health Indicator - positioned below the inline action buttons */} - {/* Only show health indicator for non-empty cells when feature is enabled */} - {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( - - )}
)}
diff --git a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx index ef403588f..dd11e0637 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx @@ -19,6 +19,7 @@ interface ValidationButtonProps { setShowSparkleButton?: Dispatch>; disabled?: boolean; disabledReason?: string; + health?: number; // Health score (0-1) for radial progress when unverified } const ValidationButton: React.FC = ({ @@ -31,6 +32,7 @@ const ValidationButton: React.FC = ({ setShowSparkleButton, disabled: externallyDisabled, disabledReason, + health, }) => { const [isValidated, setIsValidated] = useState(false); const [username, setUsername] = useState(currentUsername ?? null); @@ -380,6 +382,9 @@ const ValidationButton: React.FC = ({ currentValidations={currentValidations} requiredValidations={requiredValidations} isValidatedByCurrentUser={isValidated} + health={health} + showHealthRadial={true} // Only show radial progress for text validation + isPendingValidation={isPendingValidation} /> From 9cb1d32a4a9fd51ae259ee635d032b16d7e86e19 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 08:29:00 -0700 Subject: [PATCH 06/20] Clean up logging --- .../contextAware/contentIndexes/indexes/sqliteIndex.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts index 73ecf1088..2791984c7 100644 --- a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts +++ b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts @@ -1616,7 +1616,7 @@ export class SQLiteIndexManager { try { stmt.bind(cellIds); while (stmt.step()) { - const row = stmt.getAsObject() as { cell_id: string; t_health: number | null }; + const row = stmt.getAsObject() as { cell_id: string; t_health: number | null; }; const health = row.t_health ?? 0.3; result.set(row.cell_id, health); console.log(`[SQLiteIndex] getCellsHealth: ${row.cell_id} -> ${health} (raw: ${row.t_health})`); @@ -3760,12 +3760,11 @@ export class SQLiteIndexManager { } // Extract health from metadata (if set) + // Health is optional - if not present, it will default to 0.3 when used if (typeof metadata.health === 'number') { result.health = metadata.health; - console.log(`[SQLiteIndex] Extracted health ${metadata.health} from metadata`); - } else { - console.log(`[SQLiteIndex] No health in metadata, keys:`, Object.keys(metadata || {})); } + // No need to log when health is missing - it's expected for cells without health scores return result; } From 23359ccf90d86bde1a1207e9a7ab37d781037181 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 10:39:23 -0700 Subject: [PATCH 07/20] Enhance validation components with improved logging and state management - Added detailed logging for validation state updates in CodexCellEditor and CodexCellEditorProvider. - Refactored AudioValidationButton and ValidationButton to rely on provider state, reducing local state duplication. - Introduced health indicators in ValidatorPopover to display validation status and health information. - Cleaned up unused state variables and effects for better performance and clarity. These changes improve the user experience by providing clearer feedback during validation processes and optimizing component behavior. --- .../codexCellEditorProvider.ts | 10 +- .../CodexCellEditor/AudioValidationButton.tsx | 205 +++++-------- .../AudioValidationStatusIcon.tsx | 119 +++----- .../src/CodexCellEditor/CodexCellEditor.tsx | 158 +++++++++- .../src/CodexCellEditor/ValidationButton.tsx | 229 ++++++-------- .../components/ValidatorPopover.tsx | 286 ++++++++++++------ 6 files changed, 552 insertions(+), 455 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index e3a394cad..0aac4bd93 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -1094,10 +1094,18 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { if (docUri === document.uri.toString()) { - safePostMessageToPanel(panel, validationUpdate); + const sent = safePostMessageToPanel(panel, validationUpdate); + console.log("[CodexCellEditorProvider] Message sent:", sent, "to panel:", docUri); } }); diff --git a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx index 756d9a390..c04572741 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx @@ -1,10 +1,8 @@ import React, { useState, useEffect, useRef, Dispatch, SetStateAction } from "react"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import { QuillCellContent, ValidationEntry } from "../../../../types"; -import { getCellValueData } from "@sharedUtils"; +import { QuillCellContent } from "../../../../types"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import { processValidationQueue, enqueueValidation } from "./validationQueue"; -import { computeAudioValidationUpdate } from "./validationUtils"; import ValidationStatusIcon from "./AudioValidationStatusIcon"; import { useAudioValidationStatus } from "./hooks/useAudioValidationStatus"; import { audioPopoverTracker } from "./validationUtils"; @@ -22,6 +20,16 @@ interface AudioValidationButtonProps { setShowSparkleButton?: Dispatch>; } +/** + * AudioValidationButton - Provider is the source of truth + * + * This component relies entirely on: + * 1. The `cell` prop (updated by provider) + * 2. The `useAudioValidationStatus` hook (derives state from `cell`) + * 3. Provider messages for validation progress + * + * Local state is only used for UI-specific concerns (popover, keyboard focus, pending state). + */ const AudioValidationButton: React.FC = ({ cellId, cell, @@ -33,156 +41,97 @@ const AudioValidationButton: React.FC = ({ disabledReason, setShowSparkleButton, }) => { - const [isValidated, setIsValidated] = useState(false); - const [username, setUsername] = useState(currentUsername ?? null); - const [requiredAudioValidations, setRequiredAudioValidations] = useState( - requiredAudioValidationsProp ?? 1 - ); - const [userCreatedLatestEdit, setUserCreatedLatestEdit] = useState(false); + // UI-specific local state only const [showPopover, setShowPopover] = useState(false); const [isPendingValidation, setIsPendingValidation] = useState(false); - const [isValidationInProgress, setIsValidationInProgress] = useState(false); const [isKeyboardFocused, setIsKeyboardFocused] = useState(false); + const buttonRef = useRef(null); const closeTimerRef = useRef(null); const ignoreHoverRef = useRef(false); const wasKeyboardNavigationRef = useRef(false); - const clearCloseTimer = () => { - if (closeTimerRef.current != null) { - clearTimeout(closeTimerRef.current); - closeTimerRef.current = null; - } - }; - const scheduleCloseTimer = (callback: () => void, delay = 100) => { - clearCloseTimer(); - closeTimerRef.current = window.setTimeout(callback, delay); - }; const uniqueId = useRef( `audio-validation-${cellId}-${Math.random().toString(36).substring(2, 11)}` ); - const { iconProps: baseIconProps, validators: baseValidators } = useAudioValidationStatus({ - cell, - currentUsername: username, - requiredAudioValidations: requiredAudioValidationsProp ?? null, - isSourceText, - disabled: Boolean(externallyDisabled) || isSourceText, - displayValidationText: false, - }); - - // Create a deduplicated list of validation users - const uniqueValidationUsers = baseValidators; - const currentValidations = baseValidators.length; - - // Update validation state when attachments or hook-derived validators change - useEffect(() => { - if (!cell.attachments) { - return; - } - - const effectiveSelectedAudioId = cell.metadata?.selectedAudioId ?? ""; - - const cellValueData = getCellValueData({ - ...cell, - metadata: { - ...(cell.metadata || {}), - selectedAudioId: effectiveSelectedAudioId, - }, - } as any); - - setUserCreatedLatestEdit( - cellValueData.author === username && cellValueData.editType === "user-edit" - ); + // Use currentUsername prop directly - no local state duplication + const username = currentUsername ?? null; + + // Provider-derived state via hook - this is the source of truth + const { iconProps: baseIconProps, validators: uniqueValidationUsers } = + useAudioValidationStatus({ + cell, + currentUsername: username, + requiredAudioValidations: requiredAudioValidationsProp ?? null, + isSourceText, + disabled: Boolean(externallyDisabled) || isSourceText, + displayValidationText: false, + }); - setIsValidated(Boolean(baseIconProps.isValidatedByCurrentUser)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cell, username, baseValidators, baseIconProps.isValidatedByCurrentUser]); + // Derive from hook - no local state needed + const currentValidations = uniqueValidationUsers.length; + const isValidatedByCurrentUser = baseIconProps.isValidatedByCurrentUser; - // Get the current username when component mounts and listen for configuration changes - useEffect(() => { - if (currentUsername) { - setUsername(currentUsername); - } - }, [currentUsername]); + // Use prop directly with fallback - no local state needed + const requiredAudioValidations = requiredAudioValidationsProp ?? 1; - useEffect(() => { - if (requiredAudioValidationsProp !== undefined && requiredAudioValidationsProp !== null) { - setRequiredAudioValidations(requiredAudioValidationsProp); + const clearCloseTimer = () => { + if (closeTimerRef.current != null) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; } - }, [requiredAudioValidationsProp]); + }; - const applyValidatedByUpdate = (validatedBy: ValidationEntry[] | undefined) => { - const { isValidated: validated } = computeAudioValidationUpdate(validatedBy, username); - setIsValidated(validated); - setIsPendingValidation(false); - setIsValidationInProgress(false); + const scheduleCloseTimer = (callback: () => void, delay = 100) => { + clearCloseTimer(); + closeTimerRef.current = window.setTimeout(callback, delay); }; + // Listen for provider messages to clear pending state useMessageHandler( `audioValidationButton-${cellId}-${uniqueId.current}`, (event: MessageEvent) => { const message = event.data as any; - if (!currentUsername && message.type === "currentUsername") { - setUsername(message.content.username); - } else if ( - requiredAudioValidationsProp == null && - message.type === "validationCountAudio" - ) { - setRequiredAudioValidations(message.content); - - // The component will re-render with the new requiredAudioValidations value - // which will recalculate isFullyValidated in the render function - } else if (message.type === "providerUpdatesAudioValidationState") { - // Handle audio validation state updates from the backend + + if (message.type === "providerUpdatesAudioValidationState") { + // Provider has confirmed the validation state - clear pending if (message.content.cellId === cellId) { - applyValidatedByUpdate(message.content.validatedBy); - } - } else if (message.type === "configurationChanged") { - // Configuration changes now send validationCountAudio directly, no need to refetch - console.log("Configuration changed - audio validation count will be sent directly"); - } else if (message.command === "updateAudioValidationCount") { - if (requiredAudioValidationsProp == null) { - setRequiredAudioValidations(message.content.requiredAudioValidations || 1); + setIsPendingValidation(false); } - setIsValidated(message.content.isValidated); - setUserCreatedLatestEdit(message.content.userCreatedLatestEdit); } else if (message.type === "audioValidationInProgress") { - // Handle audio validation in progress message - if (message.content.cellId === cellId) { - setIsValidationInProgress(message.content.inProgress); - if (!message.content.inProgress) { - // If validation is complete, clear pending state as well - setIsPendingValidation(false); - } + if (message.content.cellId === cellId && !message.content.inProgress) { + setIsPendingValidation(false); } } else if (message.type === "pendingAudioValidationCleared") { if (message.content.cellIds.includes(cellId)) { setIsPendingValidation(false); } } else if (message.type === "audioHistorySelectionChanged") { - applyValidatedByUpdate(message.content.validatedBy); + // Audio selection changed - clear pending as we'll re-render with new data + if (message.content.cellId === cellId) { + setIsPendingValidation(false); + } } }, - [cellId, username, currentUsername, requiredAudioValidationsProp] + [cellId] ); + // Close popover when another becomes active useEffect(() => { if (showPopover && audioPopoverTracker.getActivePopover() !== uniqueId.current) { setShowPopover(false); } }, [showPopover]); - // Track keyboard navigation globally to detect when focus is achieved via keyboard + // Track keyboard navigation for accessibility useEffect(() => { const handleDocumentKeyDown = (e: KeyboardEvent) => { - // Track Tab, Arrow keys, and Enter as keyboard navigation if (e.key === "Tab" || e.key.startsWith("Arrow") || e.key === "Enter") { wasKeyboardNavigationRef.current = true; } }; const handleDocumentMouseDown = () => { - // Reset keyboard navigation flag when mouse is used wasKeyboardNavigationRef.current = false; }; @@ -197,8 +146,9 @@ const AudioValidationButton: React.FC = ({ const handleValidate = (e: React.MouseEvent) => { e.stopPropagation(); setIsPendingValidation(true); - // Add to audio validation queue for sequential processing - enqueueValidation(cellId, !isValidated, true) + + // Send to provider - it will update the cell, which will update the hook + enqueueValidation(cellId, !isValidatedByCurrentUser, true) .then(() => {}) .catch((error) => { console.error("Audio validation queue error:", error); @@ -214,21 +164,16 @@ const AudioValidationButton: React.FC = ({ e.stopPropagation(); if (isDisabled) return; - // briefly ignore hover so the popover can't re-open immediately ignoreHoverRef.current = true; window.setTimeout(() => { ignoreHoverRef.current = false; }, 200); - // Mark that this was a mouse click, not keyboard navigation wasKeyboardNavigationRef.current = false; setIsKeyboardFocused(false); - // Blur the button after mouse click to remove focus (prevents pulse from continuing) - // Use setTimeout to ensure blur happens after the click event completes window.setTimeout(() => { if (buttonRef.current) { - // Find the actual button element within the VSCodeButton component const buttonElement = buttonRef.current.querySelector( "button" ) as HTMLButtonElement; @@ -238,7 +183,7 @@ const AudioValidationButton: React.FC = ({ } }, 0); - if (!isValidated) { + if (!isValidatedByCurrentUser) { handleValidate(e); handleRequestClose(); } @@ -248,7 +193,6 @@ const AudioValidationButton: React.FC = ({ e.stopPropagation(); if (isDisabled) return; - if (ignoreHoverRef.current) return; clearCloseTimer(); @@ -258,7 +202,6 @@ const AudioValidationButton: React.FC = ({ const handleKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); - // Mark that keyboard navigation is being used wasKeyboardNavigationRef.current = true; if (e.key === "Enter") { @@ -274,7 +217,6 @@ const AudioValidationButton: React.FC = ({ }; const handleFocus = () => { - // If focus was achieved via keyboard, add the class for pulse animation if (wasKeyboardNavigationRef.current) { setIsKeyboardFocused(true); } @@ -314,8 +256,11 @@ const AudioValidationButton: React.FC = ({ justifyContent: "center", } as const; + // Show as in-progress when pending (waiting for provider response) + const isValidationInProgress = isPendingValidation; const isDisabled = isSourceText || isValidationInProgress || Boolean(externallyDisabled); + // Don't show validation button for source text or if no username is available if (isSourceText || !username) { return null; } @@ -334,7 +279,7 @@ const AudioValidationButton: React.FC = ({ appearance="icon" style={{ ...buttonStyle, - // Add orange border for pending validations - use a consistent orange color + // Add orange border for pending validations ...(isPendingValidation && { border: "2px solid #f5a623", borderRadius: "50%", @@ -345,18 +290,24 @@ const AudioValidationButton: React.FC = ({ onFocus={handleFocus} onBlur={handleBlur} disabled={isDisabled} - title={isDisabled ? disabledReason || "Audio validation requires audio" : undefined} + title={ + isPendingValidation + ? "Validating audio..." + : isDisabled + ? disabledReason || "Audio validation requires audio" + : undefined + } > - {/* Popover for validation users */} + {/* Popover for validation users - uses hook data directly */} {showPopover && uniqueValidationUsers.length > 0 && ( = ({ cancelCloseTimer={clearCloseTimer} scheduleCloseTimer={scheduleCloseTimer} onRemoveSelf={() => { + setIsPendingValidation(true); enqueueValidation(cellId, false, true) .then(() => {}) - .catch((error) => - console.error("Audio validation queue error:", error) - ); - processValidationQueue(vscode, true).catch((error) => - console.error("Audio validation queue processing error:", error) - ); + .catch((error) => { + console.error("Audio validation queue error:", error); + setIsPendingValidation(false); + }); + processValidationQueue(vscode, true).catch((error) => { + console.error("Audio validation queue processing error:", error); + setIsPendingValidation(false); + }); handleRequestClose(); }} /> )} - {/* Add style for spinner animation */} - {/* Popover for validation users */} - {showPopover && uniqueValidationUsers.length > 0 && ( + {/* Popover for validation users OR health status - uses hook data directly */} + {showPopover && ( = ({ cancelCloseTimer={() => {}} scheduleCloseTimer={scheduleCloseTimer} onRemoveSelf={() => { + setIsPendingValidation(true); enqueueValidation(cellId, false) .then(() => {}) - .catch((error) => console.error("Validation queue error:", error)); - processValidationQueue(vscode).catch((error) => - console.error("Validation queue processing error:", error) - ); + .catch((error) => { + console.error("Validation queue error:", error); + setIsPendingValidation(false); + }); + processValidationQueue(vscode).catch((error) => { + console.error("Validation queue processing error:", error); + setIsPendingValidation(false); + }); closePopover(); }} title="Validators" popoverTracker={textPopoverTracker} + health={currentValidations > 0 ? 1.0 : health} + showHealthWhenNoValidators={true} + isPendingValidation={isPendingValidation} + currentValidations={currentValidations} /> )}
diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx index fb2f2932c..77d5c019f 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx @@ -18,6 +18,10 @@ interface ValidatorPopoverProps { getActivePopover: () => string | null; setActivePopover: (id: string | null) => void; }; + health?: number; // Health score (0-1) for showing when no validators + showHealthWhenNoValidators?: boolean; // Whether to show health info when validators.length === 0 + isPendingValidation?: boolean; // Whether validation is pending (waiting for backend) + currentValidations?: number; // Number of current validations } export const ValidatorPopover: React.FC = ({ @@ -33,6 +37,10 @@ export const ValidatorPopover: React.FC = ({ scheduleCloseTimer, title = "Validators", popoverTracker = audioPopoverTracker, + health, + showHealthWhenNoValidators = false, + isPendingValidation = false, + currentValidations = 0, }) => { const popoverRef = useRef(null); @@ -151,7 +159,42 @@ export const ValidatorPopover: React.FC = ({ } }; - if (!show || validators.length === 0) return null; + // Show popover if: + // 1. There are validators, OR + // 2. Validation is pending (show "Validating..."), OR + // 3. We should show health when no validators AND health data exists + const hasValidators = validators.length > 0; + const isPendingWithNoValidators = isPendingValidation && !hasValidators; + const shouldShowHealth = + showHealthWhenNoValidators && + health !== undefined && + health !== null && + !isPendingValidation && + !hasValidators; + const shouldShowPopover = hasValidators || isPendingWithNoValidators || shouldShowHealth; + + if (!show || !shouldShowPopover) return null; + + // Helper to format health information + const getHealthInfo = (h: number): { percentage: number; label: string; color: string } => { + const normalizedHealth = Math.max(0, Math.min(1, h)); + const percentage = Math.round(normalizedHealth * 100); + let label = "Unverified"; + let color = "#ef4444"; // red + + if (normalizedHealth >= 1.0) { + label = "Validated"; + color = "#22c55e"; // green + } else if (normalizedHealth >= 0.7) { + label = "High confidence"; + color = "#22c55e"; // green + } else if (normalizedHealth >= 0.3) { + label = "Medium confidence"; + color = "#eab308"; // yellow + } + + return { percentage, label, color }; + }; return (
= ({ onKeyDown={handleKeyDown} >
-
{title}
+
+ {hasValidators + ? title + : isPendingWithNoValidators + ? "Validating..." + : "Health Status"} +
= ({
- {validators.map((user) => { - const isCurrentUser = user.username === currentUsername; + {isPendingWithNoValidators ? ( + // Show pending state while waiting for validation to complete +
+ + Waiting for validation... +
+ ) : hasValidators ? ( + validators.map((user) => { + const isCurrentUser = user.username === currentUsername; - return ( -
-
-
- - {user.username} - - - {formatTimestamp(user.updatedTimestamp)} - -
+ return ( +
+
+
+ + {user.username} + + + {formatTimestamp(user.updatedTimestamp)} + +
- {isCurrentUser && onRemoveSelf && ( - { - e.stopPropagation(); - onRemoveSelf(); - setShow(false); - if (popoverTracker.getActivePopover() === uniqueId) { - popoverTracker.setActivePopover(null); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); + {isCurrentUser && onRemoveSelf && ( + { + e.stopPropagation(); onRemoveSelf(); setShow(false); - } - }} - title="Remove your audio validation" - className="audio-validation-trash-icon flex items-start justify-center cursor-pointer h-8" + if ( + popoverTracker.getActivePopover() === uniqueId + ) { + popoverTracker.setActivePopover(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + onRemoveSelf(); + setShow(false); + } + }} + title="Remove your audio validation" + className="audio-validation-trash-icon flex items-start justify-center cursor-pointer h-8" + style={{ + transition: "background-color 0.2s", + }} + > + + + + + + + + )} +
+
+ ); + }) + ) : shouldShowHealth && health !== undefined ? ( + // Show health information when no validators +
+ {(() => { + const healthInfo = getHealthInfo(health); + return ( + <> +
+ Confidence: + + {healthInfo.percentage}% + +
+
+ Status: + + {healthInfo.label} + +
+
- - - - - - - - )} -
-
- ); - })} + No validators yet +
+ + ); + })()} +
+ ) : null}
+
); }; From 6b07d344a8a20966a8ff01024d7583dfcb58d3fe Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 11:48:57 -0700 Subject: [PATCH 08/20] Implement health indicators and enhance logging in CodexCellEditor - Added functionality to update health indicators based on configuration changes in CodexCellEditorProvider. - Improved logging for document change events and validation state updates in CodexCellEditor. - Updated ValidationButton and related components to conditionally display health indicators based on new props. - Refactored health management logic in CodexDocument to ensure accurate health status during validation. These enhancements provide better user feedback and improve the overall validation experience. --- .../codexCellEditorProvider.ts | 22 +++++++++++++++++ .../codexCellEditorProvider/codexDocument.ts | 4 ++++ .../CodexCellEditor/CellContentDisplay.tsx | 9 ++----- .../src/CodexCellEditor/CodexCellEditor.tsx | 24 +++++++++++++++++++ .../src/CodexCellEditor/ValidationButton.tsx | 13 +++++++--- .../hooks/useVSCodeMessageHandler.ts | 7 ++++++ 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index 0aac4bd93..dd96f35bf 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -294,6 +294,18 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider("showHealthIndicators", false); + this.webviewPanels.forEach((panel) => { + this.postMessageToWebview(panel, { + type: "updateShowHealthIndicators", + showHealthIndicators: showHealthIndicators, + }); + }); + } }); this.context.subscriptions.push(configurationChangeDisposable); @@ -1082,6 +1094,16 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0) { + console.log("[CodexCellEditorProvider] Document change event:", { + editType: e.edits[0].type, + cellId: e.edits[0].cellId, + hasHealth: "health" in e.edits[0], + health: (e.edits[0] as any).health, + }); + } + // Check if this is a validation update if (e.edits && e.edits.length > 0 && e.edits[0].type === "validation") { // Broadcast the validation update to all webviews for this document diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index 4fd15f082..af9d6796f 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -2368,6 +2368,10 @@ export class CodexCellDocument implements vscode.CustomDocument { if (activeValidations.length === 0) { // No validations remain, reset health to baseline cellToUpdate.metadata.health = 0.3; + } else { + // Still have validations, health should remain at 1.0 + // Explicitly set it to ensure it's updated in the event + cellToUpdate.metadata.health = 1.0; } } diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 42d74e558..f7d824927 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -570,13 +570,7 @@ const CellContentDisplay: React.FC = React.memo( }); cellRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); } - }, [ - cellIds, - checkShouldHighlight, - highlightedCellId, - isSourceText, - scrollSyncEnabled, - ]); + }, [cellIds, checkShouldHighlight, highlightedCellId, isSourceText, scrollSyncEnabled]); // Handler for stopping translation when clicked on the spinner const handleStopTranslation = (e: React.MouseEvent) => { @@ -1218,6 +1212,7 @@ const CellContentDisplay: React.FC = React.memo( : undefined; })()} health={cell.metadata?.health} + showHealthIndicators={showHealthIndicators} />
)} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index f2b3977a6..b579dc091 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -1164,6 +1164,20 @@ const CodexCellEditor: React.FC = () => { (event: MessageEvent) => { const message = event.data; + // Debug: log ALL messages with providerUpdatesValidationState type + if (message.type === "providerUpdatesValidationState") { + console.log( + "[CodexCellEditor] ✅ Handler received providerUpdatesValidationState:", + { + type: message.type, + cellId: message.content?.cellId, + health: message.content?.health, + validatedByCount: message.content?.validatedBy?.length || 0, + fullMessage: message, + } + ); + } + // Listen for batch validation completion if (message.type === "validationsApplied") { // Refresh progress for current milestone after batch validations are applied @@ -1186,6 +1200,16 @@ const CodexCellEditor: React.FC = () => { // Update cell health AND validatedBy in translationUnits when validation state changes if (message.content?.cellId) { setTranslationUnits((prevUnits) => { + // Check if cell exists in current units + const cellExists = prevUnits.some( + (u) => u.cellMarkers[0] === message.content.cellId + ); + console.log("[CodexCellEditor] Updating translationUnits:", { + cellId: message.content.cellId, + translationUnitsCount: prevUnits.length, + cellExists, + }); + const updated = prevUnits.map((unit) => { if (unit.cellMarkers[0] === message.content.cellId) { console.log( diff --git a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx index 7d37af524..9d9db0b94 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx @@ -19,6 +19,7 @@ interface ValidationButtonProps { disabled?: boolean; disabledReason?: string; health?: number; // Health score (0-1) for radial progress when unverified + showHealthIndicators?: boolean; // Whether to show health indicators } /** @@ -42,6 +43,7 @@ const ValidationButton: React.FC = ({ disabled: externallyDisabled, disabledReason, health, + showHealthIndicators = false, }) => { // UI-specific local state only const [showPopover, setShowPopover] = useState(false); @@ -74,17 +76,22 @@ const ValidationButton: React.FC = ({ useEffect(() => { console.log("[ValidationButton] State update:", { cellId, - health, + healthProp: health, + healthFromCell: cell.metadata?.health, + healthMatch: health === cell.metadata?.health, currentValidations, isValidatedByCurrentUser, validatorsCount: uniqueValidationUsers.length, + showHealthIndicators, }); }, [ cellId, health, + cell.metadata?.health, currentValidations, isValidatedByCurrentUser, uniqueValidationUsers.length, + showHealthIndicators, ]); // Use prop directly with fallback - no local state needed @@ -318,7 +325,7 @@ const ValidationButton: React.FC = ({ requiredValidations={requiredValidations} isValidatedByCurrentUser={isValidatedByCurrentUser} health={health} - showHealthRadial={true} + showHealthRadial={showHealthIndicators} isPendingValidation={isPendingValidation} /> @@ -365,7 +372,7 @@ const ValidationButton: React.FC = ({ title="Validators" popoverTracker={textPopoverTracker} health={currentValidations > 0 ? 1.0 : health} - showHealthWhenNoValidators={true} + showHealthWhenNoValidators={showHealthIndicators} isPendingValidation={isPendingValidation} currentValidations={currentValidations} /> diff --git a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts index 4bfe69658..97de837d7 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts +++ b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts @@ -315,6 +315,13 @@ export const useVSCodeMessageHandler = ({ } break; + case "updateShowHealthIndicators": + // Update health indicators setting + if (setShowHealthIndicators && typeof message.showHealthIndicators === 'boolean') { + setShowHealthIndicators(message.showHealthIndicators); + } + break; + case "providerSendsInitialContentPaginated": if (typeof (message as any).rev === "number") { const msgRev = (message as any).rev as number; From 49814315fa9c083214cf6a9b91ba4c34dd49bfa7 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Thu, 5 Feb 2026 19:32:55 -0700 Subject: [PATCH 09/20] Add new message type for health indicators in EditorReceiveMessages --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index c8dddc3ea..7310455ac 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2221,6 +2221,7 @@ type EditorReceiveMessages = | { type: "currentUsername"; content: { username: string; }; } | { type: "validationCount"; content: number; } | { type: "validationCountAudio"; content: number; } + | { type: "updateShowHealthIndicators"; showHealthIndicators: boolean; } | { type: "configurationChanged"; } | { type: "validationInProgress"; From 05a3b8a0af6ae6a5978b3d7178a5fb7cb6097322 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 20 Jan 2026 13:47:07 -0600 Subject: [PATCH 10/20] first draft of a health system --- .../contentIndexes/indexes/sqliteIndex.ts | 56 +++++++++-- .../codexCellEditorProvider.ts | 49 ++++++++-- .../codexCellEditorProvider/codexDocument.ts | 76 ++++++++++++++- .../utils/cellUtils.ts | 1 + .../navigationWebviewProvider.ts | 18 ++++ .../translationSuggestions/llmCompletion.ts | 15 ++- types/index.d.ts | 2 + .../CodexCellEditor/CellContentDisplay.tsx | 19 ++-- .../src/CodexCellEditor/CodexCellEditor.tsx | 32 +++++++ .../src/CodexCellEditor/HealthIndicator.tsx | 93 +++++++++++++++++++ .../src/NavigationView/index.tsx | 48 +++++++++- 11 files changed, 379 insertions(+), 30 deletions(-) create mode 100644 webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx diff --git a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts index 0503d8080..c27aabfc0 100644 --- a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts +++ b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts @@ -15,7 +15,7 @@ const debug = (message: string, ...args: any[]) => { }; // Schema version for migrations -export const CURRENT_SCHEMA_VERSION = 12; // Added milestone_index column for O(1) milestone lookup +export const CURRENT_SCHEMA_VERSION = 12; // Added milestone_index and t_health columns export class SQLiteIndexManager { private sql: SqlJsStatic | null = null; @@ -302,10 +302,13 @@ export class SQLiteIndexManager { t_audio_validation_count INTEGER DEFAULT 0, t_audio_validated_by TEXT, t_audio_is_fully_validated BOOLEAN DEFAULT FALSE, - + -- Milestone index for O(1) lookup (0-based milestone index, NULL if no milestone) milestone_index INTEGER, - + + -- Health score for translation quality (0.0-1.0, default 0.3 for unverified) + t_health REAL DEFAULT 0.3, + FOREIGN KEY (s_file_id) REFERENCES files(id) ON DELETE SET NULL, FOREIGN KEY (t_file_id) REFERENCES files(id) ON DELETE SET NULL ) @@ -866,7 +869,7 @@ export class SQLiteIndexManager { // Add target-specific metadata columns if (cellType === 'target') { columns.push('t_current_edit_timestamp', 't_validation_count', 't_validated_by', 't_is_fully_validated', - 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated'); + 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated', 't_health'); values.push( actualEditTimestamp, // Only t_current_edit_timestamp for target cells (no redundant t_updated_at) extractedMetadata.validationCount || 0, @@ -874,7 +877,8 @@ export class SQLiteIndexManager { extractedMetadata.isFullyValidated ? 1 : 0, extractedMetadata.audioValidationCount || 0, extractedMetadata.audioValidatedBy || null, - extractedMetadata.audioIsFullyValidated ? 1 : 0 + extractedMetadata.audioIsFullyValidated ? 1 : 0, + extractedMetadata.health ?? 0.3 ); } @@ -1034,7 +1038,7 @@ export class SQLiteIndexManager { // Add target-specific metadata columns if (cellType === 'target') { columns.push('t_current_edit_timestamp', 't_validation_count', 't_validated_by', 't_is_fully_validated', - 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated'); + 't_audio_validation_count', 't_audio_validated_by', 't_audio_is_fully_validated', 't_health'); values.push( actualEditTimestamp, // Only t_current_edit_timestamp for target cells (no redundant t_updated_at) extractedMetadata.validationCount || 0, @@ -1042,7 +1046,8 @@ export class SQLiteIndexManager { extractedMetadata.isFullyValidated ? 1 : 0, extractedMetadata.audioValidationCount || 0, extractedMetadata.audioValidatedBy || null, - extractedMetadata.audioIsFullyValidated ? 1 : 0 + extractedMetadata.audioIsFullyValidated ? 1 : 0, + extractedMetadata.health ?? 0.3 ); } @@ -1596,6 +1601,33 @@ export class SQLiteIndexManager { return null; } + // Get health values for multiple cells + async getCellsHealth(cellIds: string[]): Promise> { + if (!this.db || cellIds.length === 0) return new Map(); + + const placeholders = cellIds.map(() => '?').join(','); + const stmt = this.db.prepare(` + SELECT cell_id, t_health + FROM cells + WHERE cell_id IN (${placeholders}) + `); + + const result = new Map(); + try { + stmt.bind(cellIds); + while (stmt.step()) { + const row = stmt.getAsObject() as { cell_id: string; t_health: number | null }; + const health = row.t_health ?? 0.3; + result.set(row.cell_id, health); + console.log(`[SQLiteIndex] getCellsHealth: ${row.cell_id} -> ${health} (raw: ${row.t_health})`); + } + } finally { + stmt.free(); + } + console.log(`[SQLiteIndex] getCellsHealth: queried ${cellIds.length} cells, found ${result.size}`); + return result; + } + // Get translation pair by cell ID async getTranslationPair(cellId: string): Promise { if (!this.db) return null; @@ -3582,6 +3614,7 @@ export class SQLiteIndexManager { audioValidationCount?: number; audioValidatedBy?: string; audioIsFullyValidated?: boolean; + health?: number; } { const result: { currentEditTimestamp?: number | null; @@ -3591,6 +3624,7 @@ export class SQLiteIndexManager { audioValidationCount?: number; audioValidatedBy?: string; audioIsFullyValidated?: boolean; + health?: number; } = {}; if (!metadata || typeof metadata !== "object" || cellType !== "target") { @@ -3645,6 +3679,14 @@ export class SQLiteIndexManager { result.currentEditTimestamp = audioDetails.latestTimestamp; } + // Extract health from metadata (if set) + if (typeof metadata.health === 'number') { + result.health = metadata.health; + console.log(`[SQLiteIndex] Extracted health ${metadata.health} from metadata`); + } else { + console.log(`[SQLiteIndex] No health in metadata, keys:`, Object.keys(metadata || {})); + } + return result; } diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index 69f2835c1..a96ced144 100755 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -46,6 +46,7 @@ import { isMatchingFilePair as isMatchingFilePairUtil, } from "../../utils/fileTypeUtils"; import { getCorrespondingSourceUri } from "../../utils/codexNotebookUtils"; +import { getSQLiteIndexManager } from "../../activationHelpers/contextAware/contentIndexes/indexes/sqliteIndexManager"; // Enable debug logging if needed const DEBUG_MODE = false; @@ -1098,6 +1099,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0 && e.edits[0].type === "audioValidation") { const selectedAudioId = document.getExplicitAudioSelection(e.edits[0].cellId) ?? undefined; // Broadcast the audio validation update to all webviews for this document @@ -1119,6 +1122,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0) { + const indexManager = getSQLiteIndexManager(); + if (indexManager) { + const healthMap = await indexManager.getCellsHealth(exampleCellIds); + console.log(`[HealthCalc] Health map size: ${healthMap.size}, values:`, Array.from(healthMap.entries())); + if (healthMap.size > 0) { + const healthValues = Array.from(healthMap.values()); + const avgHealth = healthValues.reduce((a, b) => a + b, 0) / healthValues.length; + // Ensure health is at least 0.3 (baseline) - otherwise low-health examples + // would produce even lower health for new cells + calculatedHealth = Math.max(0.3, 0.9 * avgHealth); + console.log(`[HealthCalc] avgHealth: ${avgHealth}, calculatedHealth: ${calculatedHealth}`); + } + } else { + console.log(`[HealthCalc] No index manager available`); + } + } else { + console.log(`[HealthCalc] No example cell IDs provided`); + } + // If multiple variants are present, send to the webview for selection if (completionResult && Array.isArray((completionResult as any).variants) && (completionResult as any).variants.length > 1) { const { variants, testId, testName, isAttentionCheck, correctIndex, decoyCellId } = completionResult as any; @@ -3430,7 +3461,10 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider this.isValidValidationEntry(entry) && !entry.isDeleted + ); + if (activeValidations.length === 0) { + // No validations remain, reset health to baseline + cellToUpdate.metadata.health = 0.3; + } + } + // Mark document as dirty this._isDirty = true; @@ -2317,12 +2354,14 @@ export class CodexCellDocument implements vscode.CustomDocument { cellId, type: "validation", validatedBy: latestEdit.validatedBy, + health: cellToUpdate.metadata.health, }), edits: [ { cellId, type: "validation", validatedBy: latestEdit.validatedBy, + health: cellToUpdate.metadata.health, }, ], }); @@ -2333,10 +2372,16 @@ export class CodexCellDocument implements vscode.CustomDocument { username, validationCount: latestEdit.validatedBy.filter(entry => this.isValidValidationEntry(entry) && !entry.isDeleted).length, cellHasContent: !!(cellToUpdate.value && cellToUpdate.value.trim()), - editsCount: cellToUpdate.metadata.edits.length + editsCount: cellToUpdate.metadata.edits.length, + healthAfterValidation: cellToUpdate.metadata.health }); - // Database update will happen automatically when document is saved + // Immediately sync health to SQLite so example queries get up-to-date values + if (cellToUpdate.value) { + Promise.resolve(this.addCellToIndexImmediately(cellId, cellToUpdate.value, EditType.USER_EDIT)).catch(error => { + console.error(`[CodexDocument] Failed to sync health to SQLite for cell ${cellId}:`, error); + }); + } } // Method to validate a cell's audio by a user @@ -2417,6 +2462,21 @@ export class CodexCellDocument implements vscode.CustomDocument { } cellToUpdate.metadata.attachments[attachmentId] = attachment; + // Update health based on validation state + if (validate) { + // Set health to 100% when audio is validated (audio validation = full confidence) + cellToUpdate.metadata.health = 1.0; + } else { + // When un-validating, check if any active validations remain + const activeValidations = attachment.validatedBy.filter( + (entry: any) => this.isValidValidationEntry(entry) && !entry.isDeleted + ); + if (activeValidations.length === 0) { + // No audio validations remain, reset health to baseline + cellToUpdate.metadata.health = 0.3; + } + } + // Mark document as dirty this._isDirty = true; @@ -2426,17 +2486,24 @@ export class CodexCellDocument implements vscode.CustomDocument { cellId, type: "audioValidation", validatedBy: attachment.validatedBy, + health: cellToUpdate.metadata.health, }), edits: [ { cellId, type: "audioValidation", validatedBy: attachment.validatedBy, + health: cellToUpdate.metadata.health, }, ], }); - // Database update will happen automatically when document is saved + // Immediately sync health to SQLite so example queries get up-to-date values + if (cellToUpdate.value) { + Promise.resolve(this.addCellToIndexImmediately(cellId, cellToUpdate.value, EditType.USER_EDIT)).catch(error => { + console.error(`[CodexDocument] Failed to sync health to SQLite for cell ${cellId}:`, error); + }); + } } /** @@ -3331,6 +3398,7 @@ export class CodexCellDocument implements vscode.CustomDocument { selectionTimestamp: cell.metadata?.selectionTimestamp, type: cell.metadata?.type || null, lastUpdated: Date.now(), + health: cell.metadata?.health, }; // Check if this cell has text validation data for logging diff --git a/src/providers/codexCellEditorProvider/utils/cellUtils.ts b/src/providers/codexCellEditorProvider/utils/cellUtils.ts index 8e0ac14be..728aeacb8 100644 --- a/src/providers/codexCellEditorProvider/utils/cellUtils.ts +++ b/src/providers/codexCellEditorProvider/utils/cellUtils.ts @@ -76,6 +76,7 @@ export function convertCellToQuillContent(cell: CustomNotebookCellData): QuillCe selectionTimestamp: cell.metadata?.selectionTimestamp, parentId: cell.metadata?.parentId, isLocked: cell.metadata?.isLocked, + health: cell.metadata?.health, }, }; } diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 26b90f72a..7bfb18111 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -553,6 +553,14 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const textValidationLevels = computeLevelPercents(textValidationCounts, minimumValidationsRequired); const audioValidationLevels = computeLevelPercents(audioValidationCounts, minimumAudioValidationsRequired); + // Compute average health from cells + const healthValues = unmergedCells + .map((cell) => cell.metadata?.health) + .filter((h): h is number => typeof h === 'number'); + const averageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : 0.3; // Default to baseline if no health data + const { percentTranslationsCompleted, percentAudioTranslationsCompleted, @@ -589,6 +597,7 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { audioValidationLevels, requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth, }, sortOrder, fileDisplayName: metadata?.fileDisplayName, @@ -665,6 +674,14 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const averageTextValidationLevels = avgArray('textValidationLevels', textLen); const averageAudioValidationLevels = avgArray('audioValidationLevels', audioLen); + // Compute average health for the corpus group + const healthValues = itemsInGroup + .map((item) => item.progress?.averageHealth) + .filter((h): h is number => typeof h === 'number'); + const groupAverageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : 0.3; + const sortedItems = itemsInGroup.sort((a, b) => { if (a.sortOrder && b.sortOrder) { return a.sortOrder.localeCompare(b.sortOrder); @@ -689,6 +706,7 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { audioValidationLevels: averageAudioValidationLevels, requiredTextValidations: vscode.workspace.getConfiguration("codex-project-manager").get("validationCount", 1) || 1, requiredAudioValidations: vscode.workspace.getConfiguration("codex-project-manager").get("validationCountAudio", 1) || 1, + averageHealth: groupAverageHealth, }, }); }); diff --git a/src/providers/translationSuggestions/llmCompletion.ts b/src/providers/translationSuggestions/llmCompletion.ts index 17215eb4f..2471a6ae1 100644 --- a/src/providers/translationSuggestions/llmCompletion.ts +++ b/src/providers/translationSuggestions/llmCompletion.ts @@ -64,7 +64,8 @@ function handleABTestResult( currentCellId: string, testIdPrefix: string, completionConfig: CompletionConfig, - returnHTML: boolean + returnHTML: boolean, + exampleCellIds?: string[] ): LLMCompletionResult | null { if (result && Array.isArray(result.variants) && result.variants.length === 2) { const allowHtml = Boolean(completionConfig.allowHtmlPredictions); @@ -77,6 +78,8 @@ function handleABTestResult( isAttentionCheck: result.isAttentionCheck, correctIndex: result.correctIndex, decoyCellId: result.decoyCellId, + names: result.names, + exampleCellIds, }; } return null; @@ -90,6 +93,8 @@ export interface LLMCompletionResult { isAttentionCheck?: boolean; correctIndex?: number; decoyCellId?: string; + names?: string[]; + exampleCellIds?: string[]; // IDs of cells used as few-shot examples } export async function llmCompletion( @@ -170,6 +175,10 @@ export async function llmCompletion( numberOfFewShotExamples, completionConfig.useOnlyValidatedExamples ); + + // Extract example cell IDs for health calculation + const exampleCellIds = finalExamples.map(ex => ex.cellId); + if (completionConfig.debugMode) { console.debug(`[llmCompletion] Retrieved ${finalExamples.length} few-shot examples:`, finalExamples.map(ex => ({ cellId: ex.cellId, source: ex.sourceCell?.content?.substring(0, 50) + '...', target: ex.targetCell?.content?.substring(0, 50) + '...' }))); } @@ -302,7 +311,8 @@ export async function llmCompletion( currentCellId, "attention", completionConfig, - returnHTML + returnHTML, + exampleCellIds ); if (testResult) { @@ -335,6 +345,7 @@ export async function llmCompletion( return { variants, isABTest: false, // Identical variants – UI should hide A/B controls + exampleCellIds, }; } catch (error) { // Check if this is a cancellation error and re-throw as-is diff --git a/types/index.d.ts b/types/index.d.ts index ba06ce09d..c007e525b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -951,6 +951,7 @@ type CustomCellMetaData = BaseCustomCellMetaData & { cellLabel?: string; selectedAudioId?: string; // Points to attachment key for explicit audio selection selectionTimestamp?: number; // Timestamp when selectedAudioId was last set + health?: number; // Cell health score 0.0-1.0 (0.3 for new/unverified cells, calculated from examples for LLM-generated) }; export type CustomNotebookCellData = Omit & { @@ -1986,6 +1987,7 @@ interface CodexItem { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; // Average health score 0-1 for all cells in the file }; sortOrder?: string; isProjectDictionary?: boolean; diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 1bd260d5d..36e65a3ff 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -21,6 +21,7 @@ import { CELL_DISPLAY_MODES } from "./CodexCellEditor"; // Import the cell displ import "./TranslationAnimations.css"; // Import the animation CSS import { useTooltip } from "./contextProviders/TooltipContext"; import CommentsBadge from "./CommentsBadge"; +import HealthIndicator from "./HealthIndicator"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import ReactMarkdown from "react-markdown"; import { @@ -1122,8 +1123,9 @@ const CellContentDisplay: React.FC = React.memo( {lineNumber} )} - {/* Audio Validation Button - show for non-source text only */} - {!isSourceText && SHOW_VALIDATION_BUTTON && ( + {/* Audio Validation Button - show only when audio exists */} + {!isSourceText && SHOW_VALIDATION_BUTTON && + audioState !== "none" && audioState !== "deletedOnly" && (
= React.memo( currentUsername={currentUsername} requiredAudioValidations={requiredAudioValidations} setShowSparkleButton={setShowSparkleButton} - disabled={ - isInTranslationProcess || - audioState === "none" || - audioState === "deletedOnly" - } + disabled={isInTranslationProcess} disabledReason={ isInTranslationProcess ? "Translation in progress" - : audioState === "none" || - audioState === "deletedOnly" - ? "Audio validation requires audio" : undefined } /> @@ -1274,6 +1269,10 @@ const CellContentDisplay: React.FC = React.memo( )}
{getAlertDot()} + {/* Health Indicator - positioned below the inline action buttons */} + {!isSourceText && ( + + )} )} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 2d6de1708..796b78b75 100755 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -1182,6 +1182,22 @@ const CodexCellEditor: React.FC = () => { // Listen for individual validation state updates to refresh progress if (message.type === "providerUpdatesValidationState") { + // Update cell health in translationUnits when validation includes health + if (message.content?.cellId && message.content?.health !== undefined) { + setTranslationUnits((prevUnits) => + prevUnits.map((unit) => + unit.cellMarkers[0] === message.content.cellId + ? { + ...unit, + metadata: { + ...unit.metadata, + health: message.content.health, + }, + } + : unit + ) + ); + } // Refresh progress for current milestone after text validation completes const milestoneIdx = currentMilestoneIndexRef.current; if (milestoneIndex && milestoneIdx < milestoneIndex.milestones.length) { @@ -1191,6 +1207,22 @@ const CodexCellEditor: React.FC = () => { // Listen for audio validation state updates to refresh progress if (message.type === "providerUpdatesAudioValidationState") { + // Update cell health in translationUnits when audio validation includes health + if (message.content?.cellId && message.content?.health !== undefined) { + setTranslationUnits((prevUnits) => + prevUnits.map((unit) => + unit.cellMarkers[0] === message.content.cellId + ? { + ...unit, + metadata: { + ...unit.metadata, + health: message.content.health, + }, + } + : unit + ) + ); + } // Refresh progress for current milestone after audio validation completes const milestoneIdx = currentMilestoneIndexRef.current; if (milestoneIndex && milestoneIdx < milestoneIndex.milestones.length) { diff --git a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx new file mode 100644 index 000000000..6454e3b4e --- /dev/null +++ b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx @@ -0,0 +1,93 @@ +import React, { useState } from "react"; + +interface HealthIndicatorProps { + health: number | undefined; + className?: string; +} + +/** + * Displays a small health indicator bar showing the translation quality score. + * - Red (0-30%): Low confidence / unverified + * - Yellow (30-70%): Medium confidence + * - Green (70-100%): High confidence / verified + */ +const HealthIndicator: React.FC = ({ health, className = "" }) => { + const [isHovered, setIsHovered] = useState(false); + + // Don't render if health is undefined + if (health === undefined) { + return null; + } + + // Clamp health to 0-1 range + const normalizedHealth = Math.max(0, Math.min(1, health)); + const percentage = Math.round(normalizedHealth * 100); + + // Determine color based on health level + const getColor = (h: number): string => { + if (h < 0.3) return "#ef4444"; // red-500 + if (h < 0.7) return "#eab308"; // yellow-500 + return "#22c55e"; // green-500 + }; + + const getBackgroundColor = (h: number): string => { + if (h < 0.3) return "rgba(239, 68, 68, 0.2)"; // red with opacity + if (h < 0.7) return "rgba(234, 179, 8, 0.2)"; // yellow with opacity + return "rgba(34, 197, 94, 0.2)"; // green with opacity + }; + + const getLabel = (h: number): string => { + if (h >= 1.0) return "Validated"; + if (h >= 0.7) return "High confidence"; + if (h >= 0.3) return "Medium confidence"; + return "Unverified"; + }; + + const color = getColor(normalizedHealth); + const backgroundColor = getBackgroundColor(normalizedHealth); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+
+
+ + {percentage}% + +
+ ); +}; + +export default HealthIndicator; diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index 05e082d5a..de2c360ae 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -7,7 +7,7 @@ import { Progress } from "../components/ui/progress"; import "../tailwind.css"; import { CodexItem } from "types"; import { Popover, PopoverContent, PopoverTrigger } from "../components/ui/popover"; -import { Languages } from "lucide-react"; +import { Languages, Heart } from "lucide-react"; import { RenameModal } from "../components/RenameModal"; // Declare the acquireVsCodeApi function @@ -611,6 +611,7 @@ function NavigationView() { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; } ) => { if (typeof progress !== "object") return null; @@ -645,8 +646,53 @@ function NavigationView() { const requiredText = progress.requiredTextValidations; const requiredAudio = progress.requiredAudioValidations; + // Health indicator - compute color and percentage + const health = progress.averageHealth ?? 0.3; + const healthPercent = Math.round(health * 100); + const getHealthColor = (h: number) => { + if (h < 0.3) return "#ef4444"; // red + if (h < 0.7) return "#eab308"; // yellow + return "#22c55e"; // green + }; + const healthColor = getHealthColor(health); + return (
+ {/* Health indicator - compact pill at the top */} +
+ +
+
+
+
+ + {healthPercent}% + +
+
+
From cae27b0852dc7834f99f61412492ad29e3f1c8c2 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 20 Jan 2026 15:05:35 -0600 Subject: [PATCH 11/20] better UI and calculation --- .../codexCellEditorProvider/codexDocument.ts | 11 +++- .../navigationWebviewProvider.ts | 10 +-- .../CodexCellEditor/CellContentDisplay.tsx | 5 +- .../src/NavigationView/index.tsx | 62 ++++++++++--------- 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index f8a2934e9..6d3162ab3 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -317,11 +317,16 @@ export class CodexCellDocument implements vscode.CustomDocument { } // Set health on cell metadata if provided (for LLM generations) - if (health !== undefined) { + // Only assign health when the cell has actual content - empty cells should not have health scores + const hasContent = newContent && newContent.trim().length > 0 && newContent !== ""; + if (health !== undefined && hasContent) { cellToUpdate.metadata.health = health; - } else if (cellToUpdate.metadata.health === undefined) { - // Initialize with base health if not set + } else if (cellToUpdate.metadata.health === undefined && hasContent) { + // Initialize with base health if not set and cell has content cellToUpdate.metadata.health = 0.3; + } else if (!hasContent) { + // Clear health if cell becomes empty + cellToUpdate.metadata.health = undefined; } // For user edits, only add the edit if content has actually changed diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 7bfb18111..03a07d84e 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -553,13 +553,15 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const textValidationLevels = computeLevelPercents(textValidationCounts, minimumValidationsRequired); const audioValidationLevels = computeLevelPercents(audioValidationCounts, minimumAudioValidationsRequired); - // Compute average health from cells + // Compute average health from cells - only include cells that have content + // Empty cells should not have health scores and should not count towards average const healthValues = unmergedCells + .filter((cell) => cell.value && cell.value.trim().length > 0 && cell.value !== "") .map((cell) => cell.metadata?.health) .filter((h): h is number => typeof h === 'number'); const averageHealth = healthValues.length > 0 ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length - : 0.3; // Default to baseline if no health data + : undefined; // No health data for files with only empty cells const { percentTranslationsCompleted, @@ -674,13 +676,13 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { const averageTextValidationLevels = avgArray('textValidationLevels', textLen); const averageAudioValidationLevels = avgArray('audioValidationLevels', audioLen); - // Compute average health for the corpus group + // Compute average health for the corpus group - only from items that have health data const healthValues = itemsInGroup .map((item) => item.progress?.averageHealth) .filter((h): h is number => typeof h === 'number'); const groupAverageHealth = healthValues.length > 0 ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length - : 0.3; + : undefined; // No health data if no items have health const sortedItems = itemsInGroup.sort((a, b) => { if (a.sortOrder && b.sortOrder) { diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 36e65a3ff..7010b1838 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -1270,8 +1270,9 @@ const CellContentDisplay: React.FC = React.memo(
{getAlertDot()} {/* Health Indicator - positioned below the inline action buttons */} - {!isSourceText && ( - + {/* Only show health indicator for non-empty cells */} + {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( + )}
)} diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index de2c360ae..3e308621b 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -647,51 +647,55 @@ function NavigationView() { const requiredAudio = progress.requiredAudioValidations; // Health indicator - compute color and percentage - const health = progress.averageHealth ?? 0.3; - const healthPercent = Math.round(health * 100); + // Only show health indicator when there's actual health data (not for empty files) + const health = progress.averageHealth; + const hasHealthData = typeof health === 'number'; + const healthPercent = hasHealthData ? Math.round(health * 100) : 0; const getHealthColor = (h: number) => { if (h < 0.3) return "#ef4444"; // red if (h < 0.7) return "#eab308"; // yellow return "#22c55e"; // green }; - const healthColor = getHealthColor(health); + const healthColor = hasHealthData ? getHealthColor(health) : "#888"; return (
- {/* Health indicator - compact pill at the top */} -
- -
+ {/* Health indicator - compact pill at the top, only shown when health data exists */} + {hasHealthData && ( +
+
+ > +
+
+ + {healthPercent}% +
- - {healthPercent}% -
-
+ )}
From 2e7499476a77ed34cd46de94cc4e439ed6462761 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Tue, 27 Jan 2026 14:17:48 -0600 Subject: [PATCH 12/20] feature flag, improved UI --- package.json | 5 ++ .../codexCellEditorProvider.ts | 6 ++ .../codexCellEditorProvider/codexDocument.ts | 21 +++++ .../navigationWebviewProvider.ts | 6 ++ types/index.d.ts | 1 + .../CodexCellEditor/CellContentDisplay.tsx | 6 +- .../src/CodexCellEditor/CellList.tsx | 4 + .../ChapterNavigationHeader.tsx | 23 ++++- .../src/CodexCellEditor/CodexCellEditor.tsx | 6 ++ .../src/CodexCellEditor/HealthIndicator.tsx | 7 +- .../components/MilestoneAccordion.tsx | 88 +++++++++++++------ .../hooks/useVSCodeMessageHandler.ts | 6 ++ .../src/NavigationView/index.tsx | 72 +++++++-------- webviews/codex-webviews/src/lib/types.ts | 1 + 14 files changed, 179 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index ccef3077b..d3dd202c6 100644 --- a/package.json +++ b/package.json @@ -537,6 +537,11 @@ "default": "", "description": "Name of the project" }, + "codex-project-manager.showHealthIndicators": { + "type": "boolean", + "default": false, + "description": "Show health indicators for translation cells and files" + }, "codex-project-manager.validationCount": { "type": "integer", "default": 1, diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index a96ced144..f723b112f 100755 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -857,6 +857,11 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider("showHealthIndicators", false); + this.postMessageToWebview(webviewPanel, { type: "providerSendsInitialContentPaginated", rev, @@ -871,6 +876,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { const progress: Record = {}; const cells = this._documentData.cells || []; @@ -1763,6 +1769,7 @@ export class CodexCellDocument implements vscode.CustomDocument { audioValidationLevels: [], requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth: undefined, }; continue; } @@ -1821,12 +1828,26 @@ export class CodexCellDocument implements vscode.CustomDocument { fullyValidatedCells ); + // Calculate average health for cells with content + const healthValues = subsectionCells + .filter((cell) => + cell.cellContent && + cell.cellContent.trim().length > 0 && + cell.cellContent !== "" + ) + .map((cell) => cell.metadata?.health) + .filter((h): h is number => typeof h === 'number'); + const averageHealth = healthValues.length > 0 + ? healthValues.reduce((sum, h) => sum + h, 0) / healthValues.length + : undefined; + progress[subsectionIdx] = { ...progressPercentages, textValidationLevels, audioValidationLevels, requiredTextValidations: minimumValidationsRequired, requiredAudioValidations: minimumAudioValidationsRequired, + averageHealth, }; } diff --git a/src/providers/navigationWebview/navigationWebviewProvider.ts b/src/providers/navigationWebview/navigationWebviewProvider.ts index 03a07d84e..d1289b783 100644 --- a/src/providers/navigationWebview/navigationWebviewProvider.ts +++ b/src/providers/navigationWebview/navigationWebviewProvider.ts @@ -830,10 +830,16 @@ export class NavigationWebviewProvider extends BaseWebviewProvider { this.serializeItem(item) ); + // Get feature flag for health indicators + const showHealthIndicators = vscode.workspace + .getConfiguration("codex-project-manager") + .get("showHealthIndicators", false); + safePostMessageToView(this._view, { command: "updateItems", codexItems: serializedCodexItems, dictionaryItems: serializedDictItems, + showHealthIndicators, }); if (this.bibleBookMap) { diff --git a/types/index.d.ts b/types/index.d.ts index c007e525b..06761c485 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2034,6 +2034,7 @@ type EditorReceiveMessages = validationCountAudio?: number; isAuthenticated?: boolean; userAccessLevel?: number; + showHealthIndicators?: boolean; } | { type: "providerSendsCellPage"; diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 7010b1838..ad01bd5b5 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -75,6 +75,7 @@ interface CellContentDisplayProps { isAudioOnly?: boolean; showInlineBacktranslations?: boolean; backtranslation?: any; + showHealthIndicators?: boolean; } const DEBUG_ENABLED = false; @@ -470,6 +471,7 @@ const CellContentDisplay: React.FC = React.memo( isAudioOnly = false, showInlineBacktranslations = false, backtranslation, + showHealthIndicators = false, }) => { // const { cellContent, timestamps, editHistory } = cell; // I don't think we use this const cellIds = cell.cellMarkers; @@ -1270,9 +1272,9 @@ const CellContentDisplay: React.FC = React.memo(
{getAlertDot()} {/* Health Indicator - positioned below the inline action buttons */} - {/* Only show health indicator for non-empty cells */} + {/* Only show health indicator for non-empty cells when feature is enabled */} {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( - + )}
)} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx index 35bcb3010..c7e59298a 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellList.tsx @@ -72,6 +72,7 @@ export interface CellListProps { currentMilestoneIndex?: number; currentSubsectionIndex?: number; cellsPerPage?: number; + showHealthIndicators?: boolean; } const DEBUG_ENABLED = false; @@ -123,6 +124,7 @@ const CellList: React.FC = ({ currentMilestoneIndex = 0, currentSubsectionIndex = 0, cellsPerPage = 50, + showHealthIndicators = false, }) => { const numberOfEmptyCellsToRender = 1; const { unsavedChanges, toggleFlashingBorder } = useContext(UnsavedChangesContext); @@ -841,6 +843,7 @@ const CellList: React.FC = ({ isAudioOnly={isAudioOnly} showInlineBacktranslations={showInlineBacktranslations} backtranslation={backtranslationsMap.get(cellMarkers[0])} + showHealthIndicators={showHealthIndicators} /> ); @@ -1015,6 +1018,7 @@ const CellList: React.FC = ({ isAudioOnly={isAudioOnly} showInlineBacktranslations={showInlineBacktranslations} backtranslation={backtranslationsMap.get(cellMarkers[0])} + showHealthIndicators={showHealthIndicators} /> ); diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index b4c88a29a..8e081df52 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -92,6 +92,7 @@ interface ChapterNavigationHeaderProps { subsectionProgress?: Record; allSubsectionProgress?: Record>; requestSubsectionProgress?: (milestoneIdx: number) => void; + showHealthIndicators?: boolean; } export function ChapterNavigationHeader({ @@ -153,6 +154,7 @@ export function ChapterNavigationHeader({ subsectionProgress, allSubsectionProgress, requestSubsectionProgress, + showHealthIndicators = false, }: // Removed onToggleCorrectionEditor since it will be a VS Code command now ChapterNavigationHeaderProps) { const [showConfirm, setShowConfirm] = useState(false); @@ -1262,7 +1264,26 @@ ChapterNavigationHeaderProps) { anchorRef={chapterTitleRef} calculateSubsectionProgress={calculateSubsectionProgress} requestSubsectionProgress={requestSubsectionProgress} - vscode={vscode} + handleEditMilestoneModalOpen={handleEditMilestoneModalOpen} + showHealthIndicators={showHealthIndicators} + /> + +
); diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 796b78b75..463ef0238 100755 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -195,6 +195,9 @@ const CodexCellEditor: React.FC = () => { ); const [backtranslationsMap, setBacktranslationsMap] = useState>(new Map()); + // Health indicator display state + const [showHealthIndicators, setShowHealthIndicators] = useState(false); + // Simplified state - now we just mirror the provider's state const [autocompletionState, setAutocompletionState] = useState<{ isProcessing: boolean; @@ -1416,6 +1419,7 @@ const CodexCellEditor: React.FC = () => { setChapterNumber(chapter); }, setAudioAttachments: setAudioAttachments, + setShowHealthIndicators: setShowHealthIndicators, showABTestVariants: (data) => { const { variants, cellId, testId, testName, names, abProbability } = data as any; const count = Array.isArray(variants) ? variants.length : 0; @@ -3027,6 +3031,7 @@ const CodexCellEditor: React.FC = () => { subsectionProgress={subsectionProgress[currentMilestoneIndex]} allSubsectionProgress={subsectionProgress} requestSubsectionProgress={requestSubsectionProgressForMilestone} + showHealthIndicators={showHealthIndicators} />
@@ -3108,6 +3113,7 @@ const CodexCellEditor: React.FC = () => { currentMilestoneIndex={currentMilestoneIndex} currentSubsectionIndex={currentSubsectionIndex} cellsPerPage={cellsPerPage} + showHealthIndicators={showHealthIndicators} />
diff --git a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx index 6454e3b4e..ccd2ac1c4 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/HealthIndicator.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; interface HealthIndicatorProps { health: number | undefined; className?: string; + show?: boolean; } /** @@ -11,11 +12,11 @@ interface HealthIndicatorProps { * - Yellow (30-70%): Medium confidence * - Green (70-100%): High confidence / verified */ -const HealthIndicator: React.FC = ({ health, className = "" }) => { +const HealthIndicator: React.FC = ({ health, className = "", show = true }) => { const [isHovered, setIsHovered] = useState(false); - // Don't render if health is undefined - if (health === undefined) { + // Don't render if health is undefined or show is false + if (health === undefined || !show) { return null; } diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx index d49dc0ccc..08583d3b1 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx @@ -10,7 +10,7 @@ import { import { ProgressDots } from "./ProgressDots"; import { deriveSubsectionPercentages, getProgressDisplay } from "../utils/progressUtils"; import MicrophoneIcon from "../../components/ui/icons/MicrophoneIcon"; -import { Languages, Check, RotateCcw } from "lucide-react"; +import { Languages, Heart } from "lucide-react"; import type { Subsection, ProgressPercentages } from "../../lib/types"; import type { MilestoneIndex, MilestoneInfo } from "../../../../../types"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; @@ -43,9 +43,11 @@ interface MilestoneAccordionProps { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; }; requestSubsectionProgress?: (milestoneIdx: number) => void; - vscode: any; + handleEditMilestoneModalOpen: () => void; + showHealthIndicators?: boolean; } export function MilestoneAccordion({ @@ -62,7 +64,8 @@ export function MilestoneAccordion({ anchorRef, calculateSubsectionProgress, requestSubsectionProgress, - vscode, + handleEditMilestoneModalOpen, + showHealthIndicators = false, }: MilestoneAccordionProps) { // Layout constants const DROPDOWN_MAX_HEIGHT_VIEWPORT_PERCENT = 60; // 60vh @@ -299,6 +302,7 @@ export function MilestoneAccordion({ audioValidationLevels: backendProgress.audioValidationLevels, requiredTextValidations: backendProgress.requiredTextValidations, requiredAudioValidations: backendProgress.requiredAudioValidations, + averageHealth: backendProgress.averageHealth, }; } @@ -320,6 +324,7 @@ export function MilestoneAccordion({ audioValidationLevels: undefined, requiredTextValidations: undefined, requiredAudioValidations: undefined, + averageHealth: undefined, }; }; @@ -856,29 +861,60 @@ export function MilestoneAccordion({ }`} > {subsection.label} - +
+ {showHealthIndicators && typeof progress.averageHealth === 'number' && ( +
+ = 0.7 + ? "var(--vscode-charts-green, #22c55e)" + : progress.averageHealth >= 0.3 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + /> + = 0.7 + ? "var(--vscode-charts-green, #22c55e)" + : progress.averageHealth >= 0.3 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + > + {Math.round(progress.averageHealth * 100)}% + +
+ )} + +
); })} diff --git a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts index d55860f12..4bfe69658 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts +++ b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts @@ -58,6 +58,7 @@ interface UseVSCodeMessageHandlerProps { singleCellTranslationFailed?: () => void; setChapterNumber?: (chapterNumber: number) => void; setAudioAttachments: Dispatch>; + setShowHealthIndicators?: Dispatch>; // A/B testing handlers showABTestVariants?: (data: { variants: string[]; cellId: string; testId: string; }) => void; @@ -109,6 +110,7 @@ export const useVSCodeMessageHandler = ({ singleCellTranslationFailed, setChapterNumber, setAudioAttachments, + setShowHealthIndicators, showABTestVariants, setContentPaginated, handleCellPage, @@ -331,6 +333,10 @@ export const useVSCodeMessageHandler = ({ message.sourceCellMap ); } + // Update health indicators setting if provided + if (setShowHealthIndicators && typeof message.showHealthIndicators === 'boolean') { + setShowHealthIndicators(message.showHealthIndicators); + } // Bootstrap audio availability from initial cells try { const units = (message.cells || []) as QuillCellContent[]; diff --git a/webviews/codex-webviews/src/NavigationView/index.tsx b/webviews/codex-webviews/src/NavigationView/index.tsx index 3e308621b..5515c867b 100644 --- a/webviews/codex-webviews/src/NavigationView/index.tsx +++ b/webviews/codex-webviews/src/NavigationView/index.tsx @@ -33,6 +33,7 @@ interface State { searchQuery: string; bibleBookMap: Map | undefined; hasReceivedInitialData: boolean; + showHealthIndicators: boolean; renameModal: { isOpen: boolean; item: CodexItem | null; @@ -176,8 +177,8 @@ function NavigationView() { previousExpandedGroups: null, searchQuery: "", bibleBookMap: undefined, - hasReceivedInitialData: false, + showHealthIndicators: false, renameModal: { isOpen: false, item: null, @@ -274,6 +275,7 @@ function NavigationView() { codexItems: processedCodexItems, dictionaryItems: message.dictionaryItems || [], hasReceivedInitialData: true, + showHealthIndicators: message.showHealthIndicators ?? false, }; }); break; @@ -646,53 +648,41 @@ function NavigationView() { const requiredText = progress.requiredTextValidations; const requiredAudio = progress.requiredAudioValidations; - // Health indicator - compute color and percentage - // Only show health indicator when there's actual health data (not for empty files) + // Health indicator - compute percentage + // Only show health indicator when there's actual health data and feature flag is enabled const health = progress.averageHealth; - const hasHealthData = typeof health === 'number'; + const hasHealthData = typeof health === 'number' && state.showHealthIndicators; const healthPercent = hasHealthData ? Math.round(health * 100) : 0; - const getHealthColor = (h: number) => { - if (h < 0.3) return "#ef4444"; // red - if (h < 0.7) return "#eab308"; // yellow - return "#22c55e"; // green - }; - const healthColor = hasHealthData ? getHealthColor(health) : "#888"; return (
- {/* Health indicator - compact pill at the top, only shown when health data exists */} + {/* Health indicator - styled to match other progress bars */} {hasHealthData && ( -
- -
-
-
+
+ + + +
+
+
+
= 70 + ? "var(--vscode-charts-green, #22c55e)" + : healthPercent >= 30 + ? "var(--vscode-charts-yellow, #eab308)" + : "var(--vscode-charts-red, #ef4444)", + }} + /> +
+
+ + {healthPercent}% + +
- - {healthPercent}% -
)} diff --git a/webviews/codex-webviews/src/lib/types.ts b/webviews/codex-webviews/src/lib/types.ts index 145319027..6f495db35 100644 --- a/webviews/codex-webviews/src/lib/types.ts +++ b/webviews/codex-webviews/src/lib/types.ts @@ -49,6 +49,7 @@ export interface ProgressPercentages { audioValidationLevels?: number[]; requiredTextValidations?: number; requiredAudioValidations?: number; + averageHealth?: number; } export interface Subsection { From 206857a4dec664fd30e1ce0982119f85f0ea2763 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Fri, 30 Jan 2026 21:47:40 -0700 Subject: [PATCH 13/20] fix: complete merge of health system with main branch features Restore missing code from conflict resolution: - Add vscode prop back to MilestoneAccordion interface and component - Add RenameModal import and milestone editing handlers to ChapterNavigationHeader - Import Check and RotateCcw icons alongside Heart Co-Authored-By: Claude Opus 4.5 --- .../ChapterNavigationHeader.tsx | 43 +++++++++++++++++++ .../components/MilestoneAccordion.tsx | 4 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index 8e081df52..c365ac8c3 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -25,6 +25,7 @@ import { } from "../components/ui/dropdown-menu"; import { Slider } from "../components/ui/slider"; import { Alert, AlertDescription } from "../components/ui/alert"; +import { RenameModal } from "../components/RenameModal"; interface ChapterNavigationHeaderProps { chapterNumber: number; @@ -162,6 +163,8 @@ ChapterNavigationHeaderProps) { const [autoDownloadAudioOnOpen, setAutoDownloadAudioOnOpenState] = useState(false); const [showMilestoneAccordion, setShowMilestoneAccordion] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [milestoneNewName, setMilestoneNewName] = useState(""); + const [showEditMilestoneModal, setShowEditMilestoneModal] = useState(false); const chapterTitleRef = useRef(null); const headerContainerRef = useRef(null); const [truncatedBookName, setTruncatedBookName] = useState(null); @@ -546,6 +549,45 @@ ChapterNavigationHeaderProps) { } }; + const handleEditMilestoneModalOpen = () => { + const currentMilestone = milestoneIndex?.milestones[currentMilestoneIndex]; + if (currentMilestone) { + setMilestoneNewName(currentMilestone.value); + setShowEditMilestoneModal(true); + } + }; + + const handleEditMilestoneModalClose = () => { + setShowEditMilestoneModal(false); + setMilestoneNewName(""); + }; + + const handleEditMilestoneModalConfirm = () => { + const currentMilestone = milestoneIndex?.milestones[currentMilestoneIndex]; + if ( + currentMilestone && + milestoneNewName.trim() !== "" && + milestoneNewName.trim() !== currentMilestone.value + ) { + // Send message to update milestone value + vscode.postMessage({ + command: "updateMilestoneValue", + content: { + milestoneIndex: currentMilestoneIndex, + newValue: milestoneNewName.trim(), + }, + }); + } + handleEditMilestoneModalClose(); + }; + + // Close accordion when rename modal opens + useEffect(() => { + if (showEditMilestoneModal) { + setShowMilestoneAccordion(false); + } + }, [showEditMilestoneModal]); + const handleFontSizeChange = (value: number[]) => { const newFontSize = value[0]; setFontSize(newFontSize); @@ -1264,6 +1306,7 @@ ChapterNavigationHeaderProps) { anchorRef={chapterTitleRef} calculateSubsectionProgress={calculateSubsectionProgress} requestSubsectionProgress={requestSubsectionProgress} + vscode={vscode} handleEditMilestoneModalOpen={handleEditMilestoneModalOpen} showHealthIndicators={showHealthIndicators} /> diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx index 08583d3b1..f6ef453de 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/MilestoneAccordion.tsx @@ -10,7 +10,7 @@ import { import { ProgressDots } from "./ProgressDots"; import { deriveSubsectionPercentages, getProgressDisplay } from "../utils/progressUtils"; import MicrophoneIcon from "../../components/ui/icons/MicrophoneIcon"; -import { Languages, Heart } from "lucide-react"; +import { Languages, Heart, Check, RotateCcw } from "lucide-react"; import type { Subsection, ProgressPercentages } from "../../lib/types"; import type { MilestoneIndex, MilestoneInfo } from "../../../../../types"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; @@ -46,6 +46,7 @@ interface MilestoneAccordionProps { averageHealth?: number; }; requestSubsectionProgress?: (milestoneIdx: number) => void; + vscode: any; handleEditMilestoneModalOpen: () => void; showHealthIndicators?: boolean; } @@ -64,6 +65,7 @@ export function MilestoneAccordion({ anchorRef, calculateSubsectionProgress, requestSubsectionProgress, + vscode, handleEditMilestoneModalOpen, showHealthIndicators = false, }: MilestoneAccordionProps) { From 75e4b09477bfa2b9a14aab8482426ad63181fbb1 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 01:41:13 -0700 Subject: [PATCH 14/20] feat: enhance audio validation with health indicators - Introduced health score and radial progress for audio validation status. - Added tooltip support for health information display. - Updated ValidationButton and ValidationStatusIcon components to utilize health data. - Cleaned up CellContentDisplay by removing the HealthIndicator component and integrating health props directly. This update improves user feedback during validation processes and enhances the overall UI experience. --- .../AudioValidationStatusIcon.tsx | 267 +++++++++++++++++- .../CodexCellEditor/CellContentDisplay.tsx | 51 ++-- .../src/CodexCellEditor/ValidationButton.tsx | 5 + 3 files changed, 285 insertions(+), 38 deletions(-) diff --git a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx index b6d241a6f..f942c3f5f 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationStatusIcon.tsx @@ -1,4 +1,5 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; +import { useTooltip } from "./contextProviders/TooltipContext"; export interface ValidationStatusIconProps { isValidationInProgress: boolean; @@ -7,8 +8,18 @@ export interface ValidationStatusIconProps { requiredValidations: number; isValidatedByCurrentUser: boolean; displayValidationText?: boolean; + health?: number; // Health score (0-1) for radial progress when unverified + showHealthRadial?: boolean; // Whether to show radial health progress (only for text validation) + isPendingValidation?: boolean; // Whether validation is pending (for animation) } +// Helper function to get health color for radial progress +const getHealthColor = (health: number): string => { + if (health < 0.3) return "#ef4444"; // red-500 + if (health < 0.7) return "#eab308"; // yellow-500 + return "#22c55e"; // green-500 +}; + const ValidationStatusIcon: React.FC = ({ isValidationInProgress, isDisabled, @@ -16,7 +27,141 @@ const ValidationStatusIcon: React.FC = ({ requiredValidations, isValidatedByCurrentUser, displayValidationText, + health, + showHealthRadial = false, // Default to false - only show for text validation + isPendingValidation = false, }) => { + // Only use tooltip if we have health data and radial progress is enabled + const shouldUseTooltip = + showHealthRadial && health !== undefined && health !== null && currentValidations === 0; + const { showTooltip, hideTooltip } = useTooltip(); + const [isHovered, setIsHovered] = useState(false); + const [animatedHealth, setAnimatedHealth] = useState(null); + const tooltipTimeoutRef = useRef(null); + const radialProgressRef = useRef(null); + const animationFrameRef = useRef(null); + const animationStartTimeRef = useRef(null); + const animationStartHealthRef = useRef(0); + + // Handle validation animation: animate health from current value to 1.0 + useEffect(() => { + // When validation starts (pending), animate health to 100% + // Continue animation even if validation completes (isValidatedByCurrentUser becomes true) + if ( + isPendingValidation && + showHealthRadial && + health !== undefined && + health !== null && + animatedHealth === null + ) { + const startHealth = Math.max(0, Math.min(1, health)); + animationStartHealthRef.current = startHealth; + animationStartTimeRef.current = Date.now(); + setAnimatedHealth(startHealth); + + const animate = () => { + if (animationStartTimeRef.current === null) return; + + const elapsed = Date.now() - animationStartTimeRef.current; + const duration = 400; // 400ms animation + const progress = Math.min(elapsed / duration, 1); + + // Ease-out cubic for smooth animation + const eased = 1 - Math.pow(1 - progress, 3); + const newHealth = + animationStartHealthRef.current + (1 - animationStartHealthRef.current) * eased; + + setAnimatedHealth(newHealth); + + if (progress < 1) { + animationFrameRef.current = requestAnimationFrame(animate); + } else { + // Animation complete - set to 1.0 and clear after a brief moment to allow transition to checkmark + setAnimatedHealth(1.0); + setTimeout(() => { + setAnimatedHealth(null); + }, 150); + } + }; + + animationFrameRef.current = requestAnimationFrame(animate); + } else if (!isPendingValidation && !isValidatedByCurrentUser && animatedHealth !== null) { + // Reset animation if validation is cancelled (pending becomes false but not validated) + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + animationStartTimeRef.current = null; + setAnimatedHealth(null); + } + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [isPendingValidation, isValidatedByCurrentUser, showHealthRadial, health, animatedHealth]); + + // Handle tooltip with 750ms delay when hovering over radial progress + useEffect(() => { + // Only show tooltip for unverified cells with health data and radial progress enabled + if (!shouldUseTooltip) { + return; + } + + const shouldShowTooltip = isHovered; + + if (shouldShowTooltip) { + tooltipTimeoutRef.current = window.setTimeout(() => { + // Double-check conditions before showing tooltip + if (!radialProgressRef.current || !isHovered) { + return; + } + + const normalizedHealth = Math.max(0, Math.min(1, health!)); + const healthPercentage = Math.round(normalizedHealth * 100); + const getHealthLabel = (h: number): string => { + if (h >= 1.0) return "Validated"; + if (h >= 0.7) return "High confidence"; + if (h >= 0.3) return "Medium confidence"; + return "Unverified"; + }; + const label = getHealthLabel(normalizedHealth); + + // Get element position for tooltip + const rect = radialProgressRef.current.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top - 5; // Position slightly above the icon + + showTooltip( +
+ {healthPercentage}% - {label} +
, + x, + y + ); + }, 750); + } else { + // Clear timeout and hide tooltip when not hovering + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current); + tooltipTimeoutRef.current = null; + } + if (!isHovered) { + hideTooltip(); + } + } + + return () => { + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current); + tooltipTimeoutRef.current = null; + } + if (!isHovered) { + hideTooltip(); + } + }; + }, [isHovered, shouldUseTooltip, health, showTooltip, hideTooltip]); if (isValidationInProgress) { return ( = ({ ); } - if (currentValidations === 0) { + // Show radial progress only when unverified (currentValidations === 0) OR during validation animation + // Once validated (currentValidations > 0) and animation complete, show checkmark instead + const isUnverified = currentValidations === 0; + const showRadialDuringAnimation = animatedHealth !== null; // Show during entire animation including when it reaches 1.0 + + if (isUnverified || showRadialDuringAnimation) { + // Show radial progress when unverified, health is available, and showHealthRadial is true (text validation only) + // Use animated health if validation is in progress, otherwise use actual health + const effectiveHealth = animatedHealth !== null ? animatedHealth : health; + const showRadialProgress = + showHealthRadial && + effectiveHealth !== undefined && + effectiveHealth !== null && + (isUnverified || showRadialDuringAnimation); + const normalizedHealth = showRadialProgress ? Math.max(0, Math.min(1, effectiveHealth)) : 0; + const healthPercentage = showRadialProgress ? Math.round(normalizedHealth * 100) : 0; + const healthColor = showRadialProgress ? getHealthColor(normalizedHealth) : undefined; + + // SVG circle parameters for radial progress + // Use slightly larger size to accommodate the radial progress ring + const iconSize = 12; // Original icon size + const containerSize = 18; // Container size to fit radial progress + const strokeWidth = 2.5; + const radius = (containerSize - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const offset = circumference - normalizedHealth * circumference; + return ( -
- +
+ {showRadialProgress ? ( +
{ + e.stopPropagation(); + setIsHovered(true); + }} + onMouseLeave={(e) => { + e.stopPropagation(); + // Small delay to prevent parent handlers from interfering + setTimeout(() => { + setIsHovered(false); + }, 10); + }} + onMouseMove={(e) => { + // Keep tooltip alive while mouse is moving over the element + e.stopPropagation(); + }} + > + {/* Radial progress circle */} + + {/* Background circle */} + + {/* Progress circle */} + + + {/* Icon in center */} + +
+ ) : ( + + )} {displayValidationText && No validators}
); diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index ad01bd5b5..286c36d4b 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -21,7 +21,6 @@ import { CELL_DISPLAY_MODES } from "./CodexCellEditor"; // Import the cell displ import "./TranslationAnimations.css"; // Import the animation CSS import { useTooltip } from "./contextProviders/TooltipContext"; import CommentsBadge from "./CommentsBadge"; -import HealthIndicator from "./HealthIndicator"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import ReactMarkdown from "react-markdown"; import { @@ -1126,26 +1125,30 @@ const CellContentDisplay: React.FC = React.memo(
)} {/* Audio Validation Button - show only when audio exists */} - {!isSourceText && SHOW_VALIDATION_BUTTON && - audioState !== "none" && audioState !== "deletedOnly" && ( -
- -
- )} + {!isSourceText && + SHOW_VALIDATION_BUTTON && + audioState !== "none" && + audioState !== "deletedOnly" && ( +
+ +
+ )} {/* Audio Play Button - show for both source and non-source text */} {audioAttachments && audioAttachments[cellIds[0]] !== undefined && @@ -1208,6 +1211,7 @@ const CellContentDisplay: React.FC = React.memo( ? "Validation disabled: no text" : undefined; })()} + health={cell.metadata?.health} />
)} @@ -1271,11 +1275,6 @@ const CellContentDisplay: React.FC = React.memo( )}
{getAlertDot()} - {/* Health Indicator - positioned below the inline action buttons */} - {/* Only show health indicator for non-empty cells when feature is enabled */} - {!isSourceText && cell.cellContent && cell.cellContent.trim() !== "" && ( - - )}
)}
diff --git a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx index ef403588f..dd11e0637 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx @@ -19,6 +19,7 @@ interface ValidationButtonProps { setShowSparkleButton?: Dispatch>; disabled?: boolean; disabledReason?: string; + health?: number; // Health score (0-1) for radial progress when unverified } const ValidationButton: React.FC = ({ @@ -31,6 +32,7 @@ const ValidationButton: React.FC = ({ setShowSparkleButton, disabled: externallyDisabled, disabledReason, + health, }) => { const [isValidated, setIsValidated] = useState(false); const [username, setUsername] = useState(currentUsername ?? null); @@ -380,6 +382,9 @@ const ValidationButton: React.FC = ({ currentValidations={currentValidations} requiredValidations={requiredValidations} isValidatedByCurrentUser={isValidated} + health={health} + showHealthRadial={true} // Only show radial progress for text validation + isPendingValidation={isPendingValidation} /> From 0f698ade2efec411f168b275dd1aeb92d5d63662 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 08:29:00 -0700 Subject: [PATCH 15/20] Clean up logging --- .../contextAware/contentIndexes/indexes/sqliteIndex.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts index c27aabfc0..2f83fce97 100644 --- a/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts +++ b/src/activationHelpers/contextAware/contentIndexes/indexes/sqliteIndex.ts @@ -1616,7 +1616,7 @@ export class SQLiteIndexManager { try { stmt.bind(cellIds); while (stmt.step()) { - const row = stmt.getAsObject() as { cell_id: string; t_health: number | null }; + const row = stmt.getAsObject() as { cell_id: string; t_health: number | null; }; const health = row.t_health ?? 0.3; result.set(row.cell_id, health); console.log(`[SQLiteIndex] getCellsHealth: ${row.cell_id} -> ${health} (raw: ${row.t_health})`); @@ -3680,12 +3680,11 @@ export class SQLiteIndexManager { } // Extract health from metadata (if set) + // Health is optional - if not present, it will default to 0.3 when used if (typeof metadata.health === 'number') { result.health = metadata.health; - console.log(`[SQLiteIndex] Extracted health ${metadata.health} from metadata`); - } else { - console.log(`[SQLiteIndex] No health in metadata, keys:`, Object.keys(metadata || {})); } + // No need to log when health is missing - it's expected for cells without health scores return result; } From 3c2e7ddfadecc51ccce6f025f928f7295b88632d Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 10:39:23 -0700 Subject: [PATCH 16/20] Enhance validation components with improved logging and state management - Added detailed logging for validation state updates in CodexCellEditor and CodexCellEditorProvider. - Refactored AudioValidationButton and ValidationButton to rely on provider state, reducing local state duplication. - Introduced health indicators in ValidatorPopover to display validation status and health information. - Cleaned up unused state variables and effects for better performance and clarity. These changes improve the user experience by providing clearer feedback during validation processes and optimizing component behavior. --- .../codexCellEditorProvider.ts | 10 +- .../CodexCellEditor/AudioValidationButton.tsx | 205 +++++-------- .../AudioValidationStatusIcon.tsx | 119 +++----- .../src/CodexCellEditor/CodexCellEditor.tsx | 158 +++++++++- .../src/CodexCellEditor/ValidationButton.tsx | 229 ++++++-------- .../components/ValidatorPopover.tsx | 286 ++++++++++++------ 6 files changed, 552 insertions(+), 455 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index f723b112f..a0cd6212a 100755 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -1109,10 +1109,18 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { if (docUri === document.uri.toString()) { - safePostMessageToPanel(panel, validationUpdate); + const sent = safePostMessageToPanel(panel, validationUpdate); + console.log("[CodexCellEditorProvider] Message sent:", sent, "to panel:", docUri); } }); diff --git a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx index 756d9a390..c04572741 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/AudioValidationButton.tsx @@ -1,10 +1,8 @@ import React, { useState, useEffect, useRef, Dispatch, SetStateAction } from "react"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import { QuillCellContent, ValidationEntry } from "../../../../types"; -import { getCellValueData } from "@sharedUtils"; +import { QuillCellContent } from "../../../../types"; import { useMessageHandler } from "./hooks/useCentralizedMessageDispatcher"; import { processValidationQueue, enqueueValidation } from "./validationQueue"; -import { computeAudioValidationUpdate } from "./validationUtils"; import ValidationStatusIcon from "./AudioValidationStatusIcon"; import { useAudioValidationStatus } from "./hooks/useAudioValidationStatus"; import { audioPopoverTracker } from "./validationUtils"; @@ -22,6 +20,16 @@ interface AudioValidationButtonProps { setShowSparkleButton?: Dispatch>; } +/** + * AudioValidationButton - Provider is the source of truth + * + * This component relies entirely on: + * 1. The `cell` prop (updated by provider) + * 2. The `useAudioValidationStatus` hook (derives state from `cell`) + * 3. Provider messages for validation progress + * + * Local state is only used for UI-specific concerns (popover, keyboard focus, pending state). + */ const AudioValidationButton: React.FC = ({ cellId, cell, @@ -33,156 +41,97 @@ const AudioValidationButton: React.FC = ({ disabledReason, setShowSparkleButton, }) => { - const [isValidated, setIsValidated] = useState(false); - const [username, setUsername] = useState(currentUsername ?? null); - const [requiredAudioValidations, setRequiredAudioValidations] = useState( - requiredAudioValidationsProp ?? 1 - ); - const [userCreatedLatestEdit, setUserCreatedLatestEdit] = useState(false); + // UI-specific local state only const [showPopover, setShowPopover] = useState(false); const [isPendingValidation, setIsPendingValidation] = useState(false); - const [isValidationInProgress, setIsValidationInProgress] = useState(false); const [isKeyboardFocused, setIsKeyboardFocused] = useState(false); + const buttonRef = useRef(null); const closeTimerRef = useRef(null); const ignoreHoverRef = useRef(false); const wasKeyboardNavigationRef = useRef(false); - const clearCloseTimer = () => { - if (closeTimerRef.current != null) { - clearTimeout(closeTimerRef.current); - closeTimerRef.current = null; - } - }; - const scheduleCloseTimer = (callback: () => void, delay = 100) => { - clearCloseTimer(); - closeTimerRef.current = window.setTimeout(callback, delay); - }; const uniqueId = useRef( `audio-validation-${cellId}-${Math.random().toString(36).substring(2, 11)}` ); - const { iconProps: baseIconProps, validators: baseValidators } = useAudioValidationStatus({ - cell, - currentUsername: username, - requiredAudioValidations: requiredAudioValidationsProp ?? null, - isSourceText, - disabled: Boolean(externallyDisabled) || isSourceText, - displayValidationText: false, - }); - - // Create a deduplicated list of validation users - const uniqueValidationUsers = baseValidators; - const currentValidations = baseValidators.length; - - // Update validation state when attachments or hook-derived validators change - useEffect(() => { - if (!cell.attachments) { - return; - } - - const effectiveSelectedAudioId = cell.metadata?.selectedAudioId ?? ""; - - const cellValueData = getCellValueData({ - ...cell, - metadata: { - ...(cell.metadata || {}), - selectedAudioId: effectiveSelectedAudioId, - }, - } as any); - - setUserCreatedLatestEdit( - cellValueData.author === username && cellValueData.editType === "user-edit" - ); + // Use currentUsername prop directly - no local state duplication + const username = currentUsername ?? null; + + // Provider-derived state via hook - this is the source of truth + const { iconProps: baseIconProps, validators: uniqueValidationUsers } = + useAudioValidationStatus({ + cell, + currentUsername: username, + requiredAudioValidations: requiredAudioValidationsProp ?? null, + isSourceText, + disabled: Boolean(externallyDisabled) || isSourceText, + displayValidationText: false, + }); - setIsValidated(Boolean(baseIconProps.isValidatedByCurrentUser)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cell, username, baseValidators, baseIconProps.isValidatedByCurrentUser]); + // Derive from hook - no local state needed + const currentValidations = uniqueValidationUsers.length; + const isValidatedByCurrentUser = baseIconProps.isValidatedByCurrentUser; - // Get the current username when component mounts and listen for configuration changes - useEffect(() => { - if (currentUsername) { - setUsername(currentUsername); - } - }, [currentUsername]); + // Use prop directly with fallback - no local state needed + const requiredAudioValidations = requiredAudioValidationsProp ?? 1; - useEffect(() => { - if (requiredAudioValidationsProp !== undefined && requiredAudioValidationsProp !== null) { - setRequiredAudioValidations(requiredAudioValidationsProp); + const clearCloseTimer = () => { + if (closeTimerRef.current != null) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; } - }, [requiredAudioValidationsProp]); + }; - const applyValidatedByUpdate = (validatedBy: ValidationEntry[] | undefined) => { - const { isValidated: validated } = computeAudioValidationUpdate(validatedBy, username); - setIsValidated(validated); - setIsPendingValidation(false); - setIsValidationInProgress(false); + const scheduleCloseTimer = (callback: () => void, delay = 100) => { + clearCloseTimer(); + closeTimerRef.current = window.setTimeout(callback, delay); }; + // Listen for provider messages to clear pending state useMessageHandler( `audioValidationButton-${cellId}-${uniqueId.current}`, (event: MessageEvent) => { const message = event.data as any; - if (!currentUsername && message.type === "currentUsername") { - setUsername(message.content.username); - } else if ( - requiredAudioValidationsProp == null && - message.type === "validationCountAudio" - ) { - setRequiredAudioValidations(message.content); - - // The component will re-render with the new requiredAudioValidations value - // which will recalculate isFullyValidated in the render function - } else if (message.type === "providerUpdatesAudioValidationState") { - // Handle audio validation state updates from the backend + + if (message.type === "providerUpdatesAudioValidationState") { + // Provider has confirmed the validation state - clear pending if (message.content.cellId === cellId) { - applyValidatedByUpdate(message.content.validatedBy); - } - } else if (message.type === "configurationChanged") { - // Configuration changes now send validationCountAudio directly, no need to refetch - console.log("Configuration changed - audio validation count will be sent directly"); - } else if (message.command === "updateAudioValidationCount") { - if (requiredAudioValidationsProp == null) { - setRequiredAudioValidations(message.content.requiredAudioValidations || 1); + setIsPendingValidation(false); } - setIsValidated(message.content.isValidated); - setUserCreatedLatestEdit(message.content.userCreatedLatestEdit); } else if (message.type === "audioValidationInProgress") { - // Handle audio validation in progress message - if (message.content.cellId === cellId) { - setIsValidationInProgress(message.content.inProgress); - if (!message.content.inProgress) { - // If validation is complete, clear pending state as well - setIsPendingValidation(false); - } + if (message.content.cellId === cellId && !message.content.inProgress) { + setIsPendingValidation(false); } } else if (message.type === "pendingAudioValidationCleared") { if (message.content.cellIds.includes(cellId)) { setIsPendingValidation(false); } } else if (message.type === "audioHistorySelectionChanged") { - applyValidatedByUpdate(message.content.validatedBy); + // Audio selection changed - clear pending as we'll re-render with new data + if (message.content.cellId === cellId) { + setIsPendingValidation(false); + } } }, - [cellId, username, currentUsername, requiredAudioValidationsProp] + [cellId] ); + // Close popover when another becomes active useEffect(() => { if (showPopover && audioPopoverTracker.getActivePopover() !== uniqueId.current) { setShowPopover(false); } }, [showPopover]); - // Track keyboard navigation globally to detect when focus is achieved via keyboard + // Track keyboard navigation for accessibility useEffect(() => { const handleDocumentKeyDown = (e: KeyboardEvent) => { - // Track Tab, Arrow keys, and Enter as keyboard navigation if (e.key === "Tab" || e.key.startsWith("Arrow") || e.key === "Enter") { wasKeyboardNavigationRef.current = true; } }; const handleDocumentMouseDown = () => { - // Reset keyboard navigation flag when mouse is used wasKeyboardNavigationRef.current = false; }; @@ -197,8 +146,9 @@ const AudioValidationButton: React.FC = ({ const handleValidate = (e: React.MouseEvent) => { e.stopPropagation(); setIsPendingValidation(true); - // Add to audio validation queue for sequential processing - enqueueValidation(cellId, !isValidated, true) + + // Send to provider - it will update the cell, which will update the hook + enqueueValidation(cellId, !isValidatedByCurrentUser, true) .then(() => {}) .catch((error) => { console.error("Audio validation queue error:", error); @@ -214,21 +164,16 @@ const AudioValidationButton: React.FC = ({ e.stopPropagation(); if (isDisabled) return; - // briefly ignore hover so the popover can't re-open immediately ignoreHoverRef.current = true; window.setTimeout(() => { ignoreHoverRef.current = false; }, 200); - // Mark that this was a mouse click, not keyboard navigation wasKeyboardNavigationRef.current = false; setIsKeyboardFocused(false); - // Blur the button after mouse click to remove focus (prevents pulse from continuing) - // Use setTimeout to ensure blur happens after the click event completes window.setTimeout(() => { if (buttonRef.current) { - // Find the actual button element within the VSCodeButton component const buttonElement = buttonRef.current.querySelector( "button" ) as HTMLButtonElement; @@ -238,7 +183,7 @@ const AudioValidationButton: React.FC = ({ } }, 0); - if (!isValidated) { + if (!isValidatedByCurrentUser) { handleValidate(e); handleRequestClose(); } @@ -248,7 +193,6 @@ const AudioValidationButton: React.FC = ({ e.stopPropagation(); if (isDisabled) return; - if (ignoreHoverRef.current) return; clearCloseTimer(); @@ -258,7 +202,6 @@ const AudioValidationButton: React.FC = ({ const handleKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); - // Mark that keyboard navigation is being used wasKeyboardNavigationRef.current = true; if (e.key === "Enter") { @@ -274,7 +217,6 @@ const AudioValidationButton: React.FC = ({ }; const handleFocus = () => { - // If focus was achieved via keyboard, add the class for pulse animation if (wasKeyboardNavigationRef.current) { setIsKeyboardFocused(true); } @@ -314,8 +256,11 @@ const AudioValidationButton: React.FC = ({ justifyContent: "center", } as const; + // Show as in-progress when pending (waiting for provider response) + const isValidationInProgress = isPendingValidation; const isDisabled = isSourceText || isValidationInProgress || Boolean(externallyDisabled); + // Don't show validation button for source text or if no username is available if (isSourceText || !username) { return null; } @@ -334,7 +279,7 @@ const AudioValidationButton: React.FC = ({ appearance="icon" style={{ ...buttonStyle, - // Add orange border for pending validations - use a consistent orange color + // Add orange border for pending validations ...(isPendingValidation && { border: "2px solid #f5a623", borderRadius: "50%", @@ -345,18 +290,24 @@ const AudioValidationButton: React.FC = ({ onFocus={handleFocus} onBlur={handleBlur} disabled={isDisabled} - title={isDisabled ? disabledReason || "Audio validation requires audio" : undefined} + title={ + isPendingValidation + ? "Validating audio..." + : isDisabled + ? disabledReason || "Audio validation requires audio" + : undefined + } > - {/* Popover for validation users */} + {/* Popover for validation users - uses hook data directly */} {showPopover && uniqueValidationUsers.length > 0 && ( = ({ cancelCloseTimer={clearCloseTimer} scheduleCloseTimer={scheduleCloseTimer} onRemoveSelf={() => { + setIsPendingValidation(true); enqueueValidation(cellId, false, true) .then(() => {}) - .catch((error) => - console.error("Audio validation queue error:", error) - ); - processValidationQueue(vscode, true).catch((error) => - console.error("Audio validation queue processing error:", error) - ); + .catch((error) => { + console.error("Audio validation queue error:", error); + setIsPendingValidation(false); + }); + processValidationQueue(vscode, true).catch((error) => { + console.error("Audio validation queue processing error:", error); + setIsPendingValidation(false); + }); handleRequestClose(); }} /> )} - {/* Add style for spinner animation */} - {/* Popover for validation users */} - {showPopover && uniqueValidationUsers.length > 0 && ( + {/* Popover for validation users OR health status - uses hook data directly */} + {showPopover && ( = ({ cancelCloseTimer={() => {}} scheduleCloseTimer={scheduleCloseTimer} onRemoveSelf={() => { + setIsPendingValidation(true); enqueueValidation(cellId, false) .then(() => {}) - .catch((error) => console.error("Validation queue error:", error)); - processValidationQueue(vscode).catch((error) => - console.error("Validation queue processing error:", error) - ); + .catch((error) => { + console.error("Validation queue error:", error); + setIsPendingValidation(false); + }); + processValidationQueue(vscode).catch((error) => { + console.error("Validation queue processing error:", error); + setIsPendingValidation(false); + }); closePopover(); }} title="Validators" popoverTracker={textPopoverTracker} + health={currentValidations > 0 ? 1.0 : health} + showHealthWhenNoValidators={true} + isPendingValidation={isPendingValidation} + currentValidations={currentValidations} /> )}
diff --git a/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx b/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx index fb2f2932c..77d5c019f 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/components/ValidatorPopover.tsx @@ -18,6 +18,10 @@ interface ValidatorPopoverProps { getActivePopover: () => string | null; setActivePopover: (id: string | null) => void; }; + health?: number; // Health score (0-1) for showing when no validators + showHealthWhenNoValidators?: boolean; // Whether to show health info when validators.length === 0 + isPendingValidation?: boolean; // Whether validation is pending (waiting for backend) + currentValidations?: number; // Number of current validations } export const ValidatorPopover: React.FC = ({ @@ -33,6 +37,10 @@ export const ValidatorPopover: React.FC = ({ scheduleCloseTimer, title = "Validators", popoverTracker = audioPopoverTracker, + health, + showHealthWhenNoValidators = false, + isPendingValidation = false, + currentValidations = 0, }) => { const popoverRef = useRef(null); @@ -151,7 +159,42 @@ export const ValidatorPopover: React.FC = ({ } }; - if (!show || validators.length === 0) return null; + // Show popover if: + // 1. There are validators, OR + // 2. Validation is pending (show "Validating..."), OR + // 3. We should show health when no validators AND health data exists + const hasValidators = validators.length > 0; + const isPendingWithNoValidators = isPendingValidation && !hasValidators; + const shouldShowHealth = + showHealthWhenNoValidators && + health !== undefined && + health !== null && + !isPendingValidation && + !hasValidators; + const shouldShowPopover = hasValidators || isPendingWithNoValidators || shouldShowHealth; + + if (!show || !shouldShowPopover) return null; + + // Helper to format health information + const getHealthInfo = (h: number): { percentage: number; label: string; color: string } => { + const normalizedHealth = Math.max(0, Math.min(1, h)); + const percentage = Math.round(normalizedHealth * 100); + let label = "Unverified"; + let color = "#ef4444"; // red + + if (normalizedHealth >= 1.0) { + label = "Validated"; + color = "#22c55e"; // green + } else if (normalizedHealth >= 0.7) { + label = "High confidence"; + color = "#22c55e"; // green + } else if (normalizedHealth >= 0.3) { + label = "Medium confidence"; + color = "#eab308"; // yellow + } + + return { percentage, label, color }; + }; return (
= ({ onKeyDown={handleKeyDown} >
-
{title}
+
+ {hasValidators + ? title + : isPendingWithNoValidators + ? "Validating..." + : "Health Status"} +
= ({
- {validators.map((user) => { - const isCurrentUser = user.username === currentUsername; + {isPendingWithNoValidators ? ( + // Show pending state while waiting for validation to complete +
+ + Waiting for validation... +
+ ) : hasValidators ? ( + validators.map((user) => { + const isCurrentUser = user.username === currentUsername; - return ( -
-
-
- - {user.username} - - - {formatTimestamp(user.updatedTimestamp)} - -
+ return ( +
+
+
+ + {user.username} + + + {formatTimestamp(user.updatedTimestamp)} + +
- {isCurrentUser && onRemoveSelf && ( - { - e.stopPropagation(); - onRemoveSelf(); - setShow(false); - if (popoverTracker.getActivePopover() === uniqueId) { - popoverTracker.setActivePopover(null); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); + {isCurrentUser && onRemoveSelf && ( + { + e.stopPropagation(); onRemoveSelf(); setShow(false); - } - }} - title="Remove your audio validation" - className="audio-validation-trash-icon flex items-start justify-center cursor-pointer h-8" + if ( + popoverTracker.getActivePopover() === uniqueId + ) { + popoverTracker.setActivePopover(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + onRemoveSelf(); + setShow(false); + } + }} + title="Remove your audio validation" + className="audio-validation-trash-icon flex items-start justify-center cursor-pointer h-8" + style={{ + transition: "background-color 0.2s", + }} + > + + + + + + + + )} +
+
+ ); + }) + ) : shouldShowHealth && health !== undefined ? ( + // Show health information when no validators +
+ {(() => { + const healthInfo = getHealthInfo(health); + return ( + <> +
+ Confidence: + + {healthInfo.percentage}% + +
+
+ Status: + + {healthInfo.label} + +
+
- - - - - - - - )} -
-
- ); - })} + No validators yet +
+ + ); + })()} +
+ ) : null}
+
); }; From e752a184a98059e20c298afe0764acce1b4ed5bd Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Wed, 4 Feb 2026 11:48:57 -0700 Subject: [PATCH 17/20] Implement health indicators and enhance logging in CodexCellEditor - Added functionality to update health indicators based on configuration changes in CodexCellEditorProvider. - Improved logging for document change events and validation state updates in CodexCellEditor. - Updated ValidationButton and related components to conditionally display health indicators based on new props. - Refactored health management logic in CodexDocument to ensure accurate health status during validation. These enhancements provide better user feedback and improve the overall validation experience. --- .../codexCellEditorProvider.ts | 22 +++++++++++++++++ .../codexCellEditorProvider/codexDocument.ts | 4 ++++ .../CodexCellEditor/CellContentDisplay.tsx | 1 + .../src/CodexCellEditor/CodexCellEditor.tsx | 24 +++++++++++++++++++ .../src/CodexCellEditor/ValidationButton.tsx | 13 +++++++--- .../hooks/useVSCodeMessageHandler.ts | 7 ++++++ 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index a0cd6212a..26c69af87 100755 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -294,6 +294,18 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider("showHealthIndicators", false); + this.webviewPanels.forEach((panel) => { + this.postMessageToWebview(panel, { + type: "updateShowHealthIndicators", + showHealthIndicators: showHealthIndicators, + }); + }); + } }); this.context.subscriptions.push(configurationChangeDisposable); @@ -1097,6 +1109,16 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider 0) { + console.log("[CodexCellEditorProvider] Document change event:", { + editType: e.edits[0].type, + cellId: e.edits[0].cellId, + hasHealth: "health" in e.edits[0], + health: (e.edits[0] as any).health, + }); + } + // Check if this is a validation update if (e.edits && e.edits.length > 0 && e.edits[0].type === "validation") { // Broadcast the validation update to all webviews for this document diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index 4fd15f082..af9d6796f 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -2368,6 +2368,10 @@ export class CodexCellDocument implements vscode.CustomDocument { if (activeValidations.length === 0) { // No validations remain, reset health to baseline cellToUpdate.metadata.health = 0.3; + } else { + // Still have validations, health should remain at 1.0 + // Explicitly set it to ensure it's updated in the event + cellToUpdate.metadata.health = 1.0; } } diff --git a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx index 286c36d4b..f7d824927 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CellContentDisplay.tsx @@ -1212,6 +1212,7 @@ const CellContentDisplay: React.FC = React.memo( : undefined; })()} health={cell.metadata?.health} + showHealthIndicators={showHealthIndicators} />
)} diff --git a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx index 05d9b3121..d11fc39ab 100755 --- a/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/CodexCellEditor.tsx @@ -1174,6 +1174,20 @@ const CodexCellEditor: React.FC = () => { (event: MessageEvent) => { const message = event.data; + // Debug: log ALL messages with providerUpdatesValidationState type + if (message.type === "providerUpdatesValidationState") { + console.log( + "[CodexCellEditor] ✅ Handler received providerUpdatesValidationState:", + { + type: message.type, + cellId: message.content?.cellId, + health: message.content?.health, + validatedByCount: message.content?.validatedBy?.length || 0, + fullMessage: message, + } + ); + } + // Listen for batch validation completion if (message.type === "validationsApplied") { // Refresh progress for current milestone after batch validations are applied @@ -1196,6 +1210,16 @@ const CodexCellEditor: React.FC = () => { // Update cell health AND validatedBy in translationUnits when validation state changes if (message.content?.cellId) { setTranslationUnits((prevUnits) => { + // Check if cell exists in current units + const cellExists = prevUnits.some( + (u) => u.cellMarkers[0] === message.content.cellId + ); + console.log("[CodexCellEditor] Updating translationUnits:", { + cellId: message.content.cellId, + translationUnitsCount: prevUnits.length, + cellExists, + }); + const updated = prevUnits.map((unit) => { if (unit.cellMarkers[0] === message.content.cellId) { console.log( diff --git a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx index 7d37af524..9d9db0b94 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ValidationButton.tsx @@ -19,6 +19,7 @@ interface ValidationButtonProps { disabled?: boolean; disabledReason?: string; health?: number; // Health score (0-1) for radial progress when unverified + showHealthIndicators?: boolean; // Whether to show health indicators } /** @@ -42,6 +43,7 @@ const ValidationButton: React.FC = ({ disabled: externallyDisabled, disabledReason, health, + showHealthIndicators = false, }) => { // UI-specific local state only const [showPopover, setShowPopover] = useState(false); @@ -74,17 +76,22 @@ const ValidationButton: React.FC = ({ useEffect(() => { console.log("[ValidationButton] State update:", { cellId, - health, + healthProp: health, + healthFromCell: cell.metadata?.health, + healthMatch: health === cell.metadata?.health, currentValidations, isValidatedByCurrentUser, validatorsCount: uniqueValidationUsers.length, + showHealthIndicators, }); }, [ cellId, health, + cell.metadata?.health, currentValidations, isValidatedByCurrentUser, uniqueValidationUsers.length, + showHealthIndicators, ]); // Use prop directly with fallback - no local state needed @@ -318,7 +325,7 @@ const ValidationButton: React.FC = ({ requiredValidations={requiredValidations} isValidatedByCurrentUser={isValidatedByCurrentUser} health={health} - showHealthRadial={true} + showHealthRadial={showHealthIndicators} isPendingValidation={isPendingValidation} /> @@ -365,7 +372,7 @@ const ValidationButton: React.FC = ({ title="Validators" popoverTracker={textPopoverTracker} health={currentValidations > 0 ? 1.0 : health} - showHealthWhenNoValidators={true} + showHealthWhenNoValidators={showHealthIndicators} isPendingValidation={isPendingValidation} currentValidations={currentValidations} /> diff --git a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts index 4bfe69658..97de837d7 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts +++ b/webviews/codex-webviews/src/CodexCellEditor/hooks/useVSCodeMessageHandler.ts @@ -315,6 +315,13 @@ export const useVSCodeMessageHandler = ({ } break; + case "updateShowHealthIndicators": + // Update health indicators setting + if (setShowHealthIndicators && typeof message.showHealthIndicators === 'boolean') { + setShowHealthIndicators(message.showHealthIndicators); + } + break; + case "providerSendsInitialContentPaginated": if (typeof (message as any).rev === "number") { const msgRev = (message as any).rev as number; From 3bb446ce06d0389091e20eba192e60fad6940515 Mon Sep 17 00:00:00 2001 From: ryderwishart Date: Thu, 5 Feb 2026 19:32:55 -0700 Subject: [PATCH 18/20] Add new message type for health indicators in EditorReceiveMessages --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index 06761c485..0cd992825 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2248,6 +2248,7 @@ type EditorReceiveMessages = | { type: "currentUsername"; content: { username: string; }; } | { type: "validationCount"; content: number; } | { type: "validationCountAudio"; content: number; } + | { type: "updateShowHealthIndicators"; showHealthIndicators: boolean; } | { type: "configurationChanged"; } | { type: "validationInProgress"; From 79b1a45013ed88e42548a9d9426e42ed192710f0 Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Wed, 11 Feb 2026 11:24:11 -0600 Subject: [PATCH 19/20] merge and fix backfill --- src/providers/codexCellEditorProvider/codexDocument.ts | 9 +++++++++ .../src/CodexCellEditor/ChapterNavigationHeader.tsx | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index af9d6796f..857ada9c8 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -1828,6 +1828,15 @@ export class CodexCellDocument implements vscode.CustomDocument { fullyValidatedCells ); + // Backfill health for cells that predate the health system + for (let i = 0; i < subsectionCells.length; i++) { + const cell = subsectionCells[i]; + if (cell.metadata && cell.metadata.health === undefined && cell.cellContent && cell.cellContent.trim().length > 0 && cell.cellContent !== "") { + const hasActiveValidation = countNonDeleted(cellWithValidatedData[i].validatedBy) > 0; + cell.metadata.health = hasActiveValidation ? 1.0 : 0.3; + } + } + // Calculate average health for cells with content const healthValues = subsectionCells .filter((cell) => diff --git a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx index c365ac8c3..552905d23 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/ChapterNavigationHeader.tsx @@ -178,10 +178,6 @@ ChapterNavigationHeaderProps) { const [fontSize, setFontSize] = useState(metadata?.fontSize || 14); const [pendingFontSize, setPendingFontSize] = useState(null); - // Edit milestone modal state - const [showEditMilestoneModal, setShowEditMilestoneModal] = useState(false); - const [milestoneNewName, setMilestoneNewName] = useState(""); - // Get subsections for the current milestone const subsections = useMemo(() => { return getSubsectionsForMilestone(currentMilestoneIndex); From d14d1349b991140bd9beb62619ef1d54ff68048c Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Wed, 11 Feb 2026 11:27:14 -0600 Subject: [PATCH 20/20] fixed type issue leftover from merge --- src/providers/translationSuggestions/llmCompletion.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/translationSuggestions/llmCompletion.ts b/src/providers/translationSuggestions/llmCompletion.ts index 2471a6ae1..b2854d4a3 100644 --- a/src/providers/translationSuggestions/llmCompletion.ts +++ b/src/providers/translationSuggestions/llmCompletion.ts @@ -60,6 +60,7 @@ function handleABTestResult( isAttentionCheck?: boolean; correctIndex?: number; decoyCellId?: string; + names?: string[]; } | null, currentCellId: string, testIdPrefix: string,