Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/azure/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ app.get('/api/brainstorm/active', (req, res) => {

// ─── Hub Comments (Investigation Wall — per-hub SSE collaboration) ───────────
//
// Mirrors the brainstorm SSE pattern above. Each hypothesis hub (SuspectedCause)
// Mirrors the brainstorm SSE pattern above. Each hypothesis hub (Hypothesis)
// on the Investigation Wall can host a live comment thread. Clients subscribe
// per `${projectId}:${hubId}` and receive `init` + `comment` events. Storage is
// in-memory (ephemeral, per-pod) with a 24h TTL — customer-owned persistence
Expand Down
8 changes: 2 additions & 6 deletions apps/azure/src/__tests__/server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ beforeAll(async () => {
// Use connection string path so we avoid DefaultAzureCredential flow
process.env.AZURE_STORAGE_CONNECTION_STRING =
'DefaultEndpointsProtocol=https;AccountName=teststorage;AccountKey=dGVzdGtleQ==;EndpointSuffix=core.windows.net';
process.env.VOICE_INPUT_ENABLED = 'true';
process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT = 'gpt-4o-mini-transcribe';

// Dynamically import so mocks are active before server.js runs
const { app } = (await import('../../server.js')) as { app: Express };
Expand Down Expand Up @@ -152,18 +154,12 @@ describe('GET /config', () => {
});

it('enables microphone permissions and runtime fields when voice input is configured', async () => {
process.env.VOICE_INPUT_ENABLED = 'true';
process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT = 'gpt-4o-mini-transcribe';

const res = await request.get('/config');
const body = JSON.parse(res.text);

expect(body.voiceInputEnabled).toBe(true);
expect(body.speechToTextDeployment).toBe('gpt-4o-mini-transcribe');
expect(res.headers['permissions-policy']).toContain('microphone=(self)');

delete process.env.VOICE_INPUT_ENABLED;
delete process.env.AI_SPEECH_TO_TEXT_DEPLOYMENT;
});
});

Expand Down
4 changes: 2 additions & 2 deletions apps/azure/src/components/editor/FrameView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const FrameView: React.FC = () => {
const setProcessContext = useProjectStore(s => s.setProcessContext);
const findings = useInvestigationStore(s => s.findings);
const questions = useInvestigationStore(s => s.questions);
const suspectedCauses = useInvestigationStore(s => s.suspectedCauses);
const hypotheses = useInvestigationStore(s => s.hypotheses);
const causalLinks = useInvestigationStore(s => s.causalLinks);
const activeHubId = useProjectStore(s => s.processContext?.processHubId ?? null);
const [priorStepStats, setPriorStepStats] =
Expand Down Expand Up @@ -152,7 +152,7 @@ const FrameView: React.FC = () => {
onFocusedInvestigation={handleFocusedInvestigation}
findings={findings}
questions={questions}
suspectedCauses={suspectedCauses}
hypotheses={hypotheses}
causalLinks={causalLinks}
onOpenWall={handleOpenWall}
onOpenInvestigationFocus={handleOpenInvestigationFocus}
Expand Down
74 changes: 36 additions & 38 deletions apps/azure/src/components/editor/InvestigationWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import {
InvestigationConclusion,
FindingsLog,
QuestionLinkPrompt,
WallCanvas,
CommandPalette,
Minimap,
CANVAS_W,
CANVAS_H,
useWallKeyboard,
useWallIsMobile,
type HubComposerBranchFields,
} from '@variscout/ui';
import {
Expand Down Expand Up @@ -45,15 +52,6 @@ import {
usePreferencesStore,
useWallLayoutStore,
} from '@variscout/stores';
import {
WallCanvas,
CommandPalette,
Minimap,
CANVAS_W,
CANVAS_H,
useWallKeyboard,
useWallIsMobile,
} from '@variscout/charts';
import { InvestigationMapView } from './InvestigationMapView';
import { CoScoutSection } from './CoScoutSection';
import { isSpeechToTextAvailable, transcribeAudio } from '../../services/speechService';
Expand Down Expand Up @@ -99,8 +97,8 @@ interface InvestigationWorkspaceProps {
handleSearchKnowledge: () => void;
// Column aliases
columnAliases: Record<string, string>;
// Hub model (SuspectedCause CRUD from useInvestigationOrchestration)
suspectedCausesState: UseInvestigationOrchestrationReturn['suspectedCausesState'];
// Hub model (Hypothesis CRUD from useInvestigationOrchestration)
hypothesesState: UseInvestigationOrchestrationReturn['hypothesesState'];
// Derived investigation data (from orchestration hook)
questionsMap: Record<string, QuestionDisplayData>;
ideaImpacts: Record<string, import('@variscout/core').IdeaImpact | undefined>;
Expand Down Expand Up @@ -134,7 +132,7 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
actionProposalsState,
handleSearchKnowledge,
columnAliases,
suspectedCausesState,
hypothesesState,
questionsMap,
ideaImpacts,
viewMode: externalViewMode,
Expand Down Expand Up @@ -345,8 +343,8 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
};
}, [factorIntelQuestions]);

// ── Hub model computations (SuspectedCause hubs) ───────────────────────
const hubs = suspectedCausesState.hubs;
// ── Hub model computations (Hypothesis hubs) ───────────────────────
const hubs = hypothesesState.hubs;

// Phase 13 — pan-to-node: replicate WallCanvas's deterministic layout so the
// command palette can center the viewport on a hub/question by id.
Expand Down Expand Up @@ -392,7 +390,7 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
const handleViewMode = onViewModeChange ?? setInternalViewMode;

// Categorize questions for InvestigationConclusion
const { suspectedCauses, contributing, ruledOut } = useMemo(() => {
const { hypotheses, contributing, ruledOut } = useMemo(() => {
const suspected: Question[] = [];
const contrib: Question[] = [];
const ruled: Question[] = [];
Expand All @@ -401,7 +399,7 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
else if (h.causeRole === 'contributing') contrib.push(h);
else if (h.causeRole === 'ruled-out') ruled.push(h);
}
return { suspectedCauses: suspected, contributing: contrib, ruledOut: ruled };
return { hypotheses: suspected, contributing: contrib, ruledOut: ruled };
}, [questionsState.questions]);

const drillFactors = useMemo(() => drillPath.map(d => d.factor), [drillPath]);
Expand Down Expand Up @@ -457,7 +455,7 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
},
problemStatement,
questions: questionsState.questions,
suspectedCauseHubs: hubs,
hypothesisHubs: hubs,
onCurrentUnderstandingChange: handleCurrentUnderstandingChange,
});

Expand All @@ -481,12 +479,12 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
findingIds: string[],
branchFields: HubComposerBranchFields
) => {
const hub = suspectedCausesState.createHub(name, synthesis);
for (const qId of questionIds) suspectedCausesState.connectQuestion(hub.id, qId);
for (const fId of findingIds) suspectedCausesState.connectFinding(hub.id, fId);
if (branchFields.nextMove) suspectedCausesState.updateHub(hub.id, branchFields);
const hub = hypothesesState.createHub(name, synthesis);
for (const qId of questionIds) hypothesesState.connectQuestion(hub.id, qId);
for (const fId of findingIds) hypothesesState.connectFinding(hub.id, fId);
if (branchFields.nextMove) hypothesesState.updateHub(hub.id, branchFields);
},
[suspectedCausesState]
[hypothesesState]
);

const handleUpdateHub = useCallback(
Expand All @@ -498,48 +496,48 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
findingIds: string[],
branchFields: HubComposerBranchFields
) => {
suspectedCausesState.updateHub(hubId, { name, synthesis, ...branchFields });
hypothesesState.updateHub(hubId, { name, synthesis, ...branchFields });
// Sync connections: disconnect removed, connect added
const existing = hubs.find(h => h.id === hubId);
if (existing) {
for (const qId of existing.questionIds) {
if (!questionIds.includes(qId)) suspectedCausesState.disconnectQuestion(hubId, qId);
if (!questionIds.includes(qId)) hypothesesState.disconnectQuestion(hubId, qId);
}
for (const qId of questionIds) {
if (!existing.questionIds.includes(qId)) suspectedCausesState.connectQuestion(hubId, qId);
if (!existing.questionIds.includes(qId)) hypothesesState.connectQuestion(hubId, qId);
}
for (const fId of existing.findingIds) {
if (!findingIds.includes(fId)) suspectedCausesState.disconnectFinding(hubId, fId);
if (!findingIds.includes(fId)) hypothesesState.disconnectFinding(hubId, fId);
}
for (const fId of findingIds) {
if (!existing.findingIds.includes(fId)) suspectedCausesState.connectFinding(hubId, fId);
if (!existing.findingIds.includes(fId)) hypothesesState.connectFinding(hubId, fId);
}
}
},
[suspectedCausesState, hubs]
[hypothesesState, hubs]
);

const handleDeleteHub = useCallback(
(hubId: string) => suspectedCausesState.deleteHub(hubId),
[suspectedCausesState]
(hubId: string) => hypothesesState.deleteHub(hubId),
[hypothesesState]
);

const handleToggleHubSelect = useCallback(
(hubId: string) => {
const hub = hubs.find(h => h.id === hubId);
if (hub) {
suspectedCausesState.updateHub(hubId, {});
hypothesesState.updateHub(hubId, {});
// Toggle selectedForImprovement via setHubStatus or direct update
// The useSuspectedCauses hook manages the selectedForImprovement toggle
// The useHypotheses hook manages the selectedForImprovement toggle
// through the hub's status — but for selection we toggle the flag directly.
// Since updateHub only accepts name/synthesis, use the store sync approach:
const updated = hubs.map(h =>
h.id === hubId ? { ...h, selectedForImprovement: !h.selectedForImprovement } : h
);
suspectedCausesState.resetHubs(updated);
hypothesesState.resetHubs(updated);
}
},
[suspectedCausesState, hubs]
[hypothesesState, hubs]
);

const handleBrainstormHub = useCallback((_hubId: string) => {
Expand Down Expand Up @@ -674,14 +672,14 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
</div>

{/* Investigation conclusion */}
{(suspectedCauses.length > 0 || ruledOut.length > 0 || hubs.length > 0) && (
{(hypotheses.length > 0 || ruledOut.length > 0 || hubs.length > 0) && (
<div className="border-t border-edge px-3 py-2 flex-shrink-0">
<InvestigationConclusion
suspectedCauses={suspectedCauses}
hypotheses={hypotheses}
ruledOut={ruledOut}
contributing={contributing}
problemStatement={processContext?.problemStatement}
hasConclusions={suspectedCauses.length > 0 || hubs.length > 0}
hasConclusions={hypotheses.length > 0 || hubs.length > 0}
problemStatementDraft={problemStatement.draft}
isProblemStatementReady={problemStatement.isReady}
onGenerateProblemStatement={problemStatement.generate}
Expand Down Expand Up @@ -853,7 +851,7 @@ export const InvestigationWorkspace: React.FC<InvestigationWorkspaceProps> = ({
causalLinks,
questions: questionsState.questions,
findings: findingsState.findings,
suspectedCauses: hubs,
hypotheses: hubs,
}}
onAskQuestion={handleMapAskQuestion}
onCreateFinding={handleMapCreateFinding}
Expand Down
6 changes: 3 additions & 3 deletions apps/azure/src/components/editor/PISection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const PISection: React.FC<PISectionProps> = ({
const defectMapping = useProjectStore(s => s.defectMapping);
const processContext = useProjectStore(s => s.processContext);
const setProcessContext = useProjectStore(s => s.setProcessContext);
const suspectedCauses = useInvestigationStore(s => s.suspectedCauses);
const hypotheses = useInvestigationStore(s => s.hypotheses);

// Panel visibility and tab state from panelsStore
const isPISidebarOpen = usePanelsStore(s => s.isPISidebarOpen);
Expand Down Expand Up @@ -170,7 +170,7 @@ export const PISection: React.FC<PISectionProps> = ({
processContext: processContext ?? undefined,
questions: questionsState.questions,
findings: findingsState.findings,
branches: suspectedCauses,
branches: hypotheses,
}),
[
rawData,
Expand All @@ -183,7 +183,7 @@ export const PISection: React.FC<PISectionProps> = ({
processContext,
questionsState.questions,
findingsState.findings,
suspectedCauses,
hypotheses,
]
);

Expand Down
6 changes: 3 additions & 3 deletions apps/azure/src/components/editor/__tests__/FrameView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const investigationStateRef: { current: Record<string, unknown> } = {
current: {
findings: [],
questions: [],
suspectedCauses: [],
hypotheses: [],
causalLinks: [],
addCausalLink: addCausalLinkMock,
linkQuestionToCausalLink: linkQuestionToCausalLinkMock,
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('FrameView (Azure shell)', () => {
investigationStateRef.current = {
findings: [{ id: 'f-1' }],
questions: [{ id: 'q-1' }],
suspectedCauses: [{ id: 'hub-1' }],
hypotheses: [{ id: 'hub-1' }],
causalLinks: [{ id: 'link-1' }],
addCausalLink: addCausalLinkMock,
linkQuestionToCausalLink: linkQuestionToCausalLinkMock,
Expand All @@ -241,7 +241,7 @@ describe('FrameView (Azure shell)', () => {
setProcessContext: setProcessContextMock,
findings: [{ id: 'f-1' }],
questions: [{ id: 'q-1' }],
suspectedCauses: [{ id: 'hub-1' }],
hypotheses: [{ id: 'hub-1' }],
causalLinks: [{ id: 'link-1' }],
signals: { hasIntervention: false, sustainmentConfirmed: false },
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ vi.mock('@variscout/charts', async importOriginal => {
return {
...actual,
EvidenceMapBase: () => <div data-testid="evidence-map-base" />,
WallCanvas: (props: { hubs: unknown[] }) =>
props.hubs.length > 0 ? (
<div data-testid="wall-canvas" data-has-process-map={String('processMap' in props)} />
) : (
<div data-testid="wall-canvas-empty" data-has-process-map={String('processMap' in props)} />
),
};
});

Expand Down Expand Up @@ -82,6 +76,13 @@ vi.mock('@variscout/ui', async importOriginal => {
InvestigationConclusion: () => null,
FindingsLog: () => <div data-testid="findings-log" />,
QuestionLinkPrompt: () => null,
useWallIsMobile: () => false,
WallCanvas: (props: { hubs: unknown[] }) =>
props.hubs.length > 0 ? (
<div data-testid="wall-canvas" data-has-process-map={String('processMap' in props)} />
) : (
<div data-testid="wall-canvas-empty" data-has-process-map={String('processMap' in props)} />
),
};
});

Expand Down Expand Up @@ -227,7 +228,7 @@ function makeMinimalProps(): React.ComponentProps<typeof InvestigationWorkspace>
actionProposalsState: {} as never,
handleSearchKnowledge: noOp,
columnAliases: {},
suspectedCausesState: {
hypothesesState: {
hubs: [],
createHub: vi.fn(() => ({ id: 'hub-1' }) as never),
updateHub: noOp,
Expand Down Expand Up @@ -291,14 +292,14 @@ describe('InvestigationWorkspace Map/Wall toggle', () => {
it('renders the WallCanvas for a chart-first investigation without a process map', () => {
useWallLayoutStore.getState().setViewMode('wall');
const props = makeMinimalProps();
props.suspectedCausesState.hubs = [
props.hypothesesState.hubs = [
{
id: 'hub-1',
name: 'Nozzle heat drift',
synthesis: '',
questionIds: [],
findingIds: [],
status: 'suspected',
status: 'proposed',
createdAt: '',
updatedAt: '',
},
Expand Down
6 changes: 3 additions & 3 deletions apps/azure/src/components/views/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const ReportView: React.FC<ReportViewProps> = ({
const findings = useInvestigationStore(s => s.findings);
const questions = useInvestigationStore(s => s.questions);
const causalLinks = useInvestigationStore(s => s.causalLinks);
const suspectedCauses = useInvestigationStore(s => s.suspectedCauses);
const hypotheses = useInvestigationStore(s => s.hypotheses);

// ---------------------------------------------------------------------------
// Evidence Map computation for Report timeline
Expand Down Expand Up @@ -203,14 +203,14 @@ const ReportView: React.FC<ReportViewProps> = ({
causalLinks,
questions,
findings,
suspectedCauses: suspectedCauses ?? [],
hypotheses: hypotheses ?? [],
});

const timeline = useEvidenceMapTimeline({
causalLinks,
questions,
findings,
suspectedCauses: suspectedCauses ?? [],
hypotheses: hypotheses ?? [],
});

// ---------------------------------------------------------------------------
Expand Down
Loading