Skip to content

Commit 31ab599

Browse files
committed
refactor: tranvel planner delegates to pure functions
1 parent 3e67463 commit 31ab599

1 file changed

Lines changed: 137 additions & 75 deletions

File tree

src/app/domains/ticketing/ai/travel-planner/travel-planner-page.ts

Lines changed: 137 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
} from '@angular/core';
99
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
1010
import {
11+
type AgUiChatMessage,
1112
agUiResource,
1213
type AgUiToolCall,
14+
type AgUiWidgetInstance,
1315
type AgUiWorkflowStep,
1416
createShowComponentsTool,
1517
WidgetContainerComponent,
@@ -70,40 +72,31 @@ export class TravelPlannerPage {
7072
});
7173

7274
protected readonly widgets = computed(() =>
73-
this.chat
74-
.value()
75-
.filter((message) => message.role === 'assistant')
76-
.flatMap((message) => message.widgets),
75+
selectAssistantWidgets(this.chat.value()),
7776
);
7877

7978
protected readonly messageWidgets = computed(() =>
80-
this.widgets().filter((widget) => widget.name === 'messageWidget'),
79+
selectWidgetsByName(this.widgets(), 'messageWidget'),
8180
);
8281

8382
protected readonly flightWidgets = computed(() =>
84-
this.widgets().filter((widget) => widget.name === 'flightWidget'),
83+
selectWidgetsByName(this.widgets(), 'flightWidget'),
8584
);
8685

8786
protected readonly hotelWidgets = computed(() =>
88-
this.widgets().filter((widget) => widget.name === 'hotelWidget'),
87+
selectWidgetsByName(this.widgets(), 'hotelWidget'),
8988
);
9089

91-
protected readonly otherWidgets = computed(() => {
92-
const known = new Set(['messageWidget', 'flightWidget', 'hotelWidget']);
93-
return this.widgets().filter((widget) => !known.has(widget.name));
94-
});
90+
protected readonly otherWidgets = computed(() =>
91+
selectOtherWidgets(this.widgets()),
92+
);
9593

96-
protected readonly errorMessage = computed<string | null>(() => {
97-
const error = this.chat.value().find((message) => message.role === 'error');
98-
return error?.content ?? null;
99-
});
94+
protected readonly errorMessage = computed<string | null>(() =>
95+
readErrorMessage(this.chat.value()),
96+
);
10097

10198
protected readonly toolCalls = computed<AgUiToolCall[]>(() =>
102-
this.chat
103-
.value()
104-
.filter((message) => message.role === 'assistant')
105-
.flatMap((message) => message.toolCalls)
106-
.filter((toolCall) => toolCall.name !== 'showComponents'),
99+
selectVisibleToolCalls(this.chat.value()),
107100
);
108101

109102
/**
@@ -112,61 +105,27 @@ export class TravelPlannerPage {
112105
* small extra section below the step timeline so they aren't lost.
113106
*/
114107
protected readonly topLevelToolCalls = computed<AgUiToolCall[]>(() =>
115-
this.toolCalls().filter(
116-
(toolCall) =>
117-
!toolCall.stepName && !toolCall.name.startsWith('workflow-'),
118-
),
108+
selectTopLevelToolCalls(this.toolCalls()),
119109
);
120110

121111
protected readonly workflowSteps = computed<AgUiWorkflowStep[]>(() =>
122-
this.chat
123-
.value()
124-
.filter((message) => message.role === 'assistant')
125-
.flatMap((message) => message.workflowSteps),
112+
selectWorkflowSteps(this.chat.value()),
126113
);
127114

128115
protected readonly toolCallsByStep = computed<Map<string, AgUiToolCall[]>>(
129-
() => {
130-
const map = new Map<string, AgUiToolCall[]>();
131-
for (const toolCall of this.toolCalls()) {
132-
const key = toolCall.stepName;
133-
if (!key) {
134-
continue;
135-
}
136-
const list = map.get(key);
137-
if (list) {
138-
list.push(toolCall);
139-
} else {
140-
map.set(key, [toolCall]);
141-
}
142-
}
143-
return map;
144-
},
116+
() => groupToolCallsByStep(this.toolCalls()),
145117
);
146118

147119
protected readonly currentWorkflowStep = computed<string | null>(() => {
148-
const steps = this.workflowSteps();
149-
for (let i = steps.length - 1; i >= 0; i -= 1) {
150-
const step = steps[i];
151-
if (step.status === 'pending') {
152-
return WORKFLOW_STEP_LABELS[step.name] ?? step.name;
153-
}
154-
}
155-
return null;
120+
return readCurrentWorkflowStep(this.workflowSteps(), WORKFLOW_STEP_LABELS);
156121
});
157122

158123
protected readonly currentStatus = computed<string>(() => {
159-
const step = this.currentWorkflowStep();
160-
if (step) {
161-
return step;
162-
}
163-
if (this.chat.isLoading()) {
164-
return 'Building travel plan';
165-
}
166-
if (this.widgets().length > 0) {
167-
return 'Done';
168-
}
169-
return 'Ready';
124+
return readCurrentStatus(
125+
this.currentWorkflowStep(),
126+
this.chat.isLoading(),
127+
this.widgets().length,
128+
);
170129
});
171130

172131
protected readonly showToolDetails = signal(false);
@@ -211,21 +170,11 @@ export class TravelPlannerPage {
211170
}
212171

213172
protected formatToolArgs(args: unknown): string {
214-
if (args === undefined || args === null) {
215-
return '';
216-
}
217-
if (typeof args === 'string') {
218-
return args;
219-
}
220-
try {
221-
return JSON.stringify(args, null, 2);
222-
} catch {
223-
return String(args);
224-
}
173+
return formatToolArgsValue(args);
225174
}
226175

227176
protected stepLabel(name: string): string {
228-
return WORKFLOW_STEP_LABELS[name] ?? name;
177+
return readStepLabel(name, WORKFLOW_STEP_LABELS);
229178
}
230179

231180
protected hasWorkflowSteps(): boolean {
@@ -236,3 +185,116 @@ export class TravelPlannerPage {
236185
return this.toolCallsByStep().get(stepName) ?? [];
237186
}
238187
}
188+
189+
function selectAssistantWidgets(
190+
messages: AgUiChatMessage[],
191+
): AgUiWidgetInstance[] {
192+
return messages
193+
.filter((message) => message.role === 'assistant')
194+
.flatMap((message) => message.widgets);
195+
}
196+
197+
function selectWidgetsByName(
198+
widgets: AgUiWidgetInstance[],
199+
name: string,
200+
): AgUiWidgetInstance[] {
201+
return widgets.filter((widget) => widget.name === name);
202+
}
203+
204+
function selectOtherWidgets(
205+
widgets: AgUiWidgetInstance[],
206+
): AgUiWidgetInstance[] {
207+
const known = new Set(['messageWidget', 'flightWidget', 'hotelWidget']);
208+
return widgets.filter((widget) => !known.has(widget.name));
209+
}
210+
211+
function readErrorMessage(messages: AgUiChatMessage[]): string | null {
212+
const error = messages.find((message) => message.role === 'error');
213+
return error?.content ?? null;
214+
}
215+
216+
function selectVisibleToolCalls(messages: AgUiChatMessage[]): AgUiToolCall[] {
217+
return messages
218+
.filter((message) => message.role === 'assistant')
219+
.flatMap((message) => message.toolCalls)
220+
.filter((toolCall) => toolCall.name !== 'showComponents');
221+
}
222+
223+
function selectTopLevelToolCalls(toolCalls: AgUiToolCall[]): AgUiToolCall[] {
224+
return toolCalls.filter(
225+
(toolCall) => !toolCall.stepName && !toolCall.name.startsWith('workflow-'),
226+
);
227+
}
228+
229+
function selectWorkflowSteps(messages: AgUiChatMessage[]): AgUiWorkflowStep[] {
230+
return messages
231+
.filter((message) => message.role === 'assistant')
232+
.flatMap((message) => message.workflowSteps);
233+
}
234+
235+
function groupToolCallsByStep(
236+
toolCalls: AgUiToolCall[],
237+
): Map<string, AgUiToolCall[]> {
238+
const map = new Map<string, AgUiToolCall[]>();
239+
for (const toolCall of toolCalls) {
240+
const key = toolCall.stepName;
241+
if (!key) {
242+
continue;
243+
}
244+
const list = map.get(key);
245+
if (list) {
246+
list.push(toolCall);
247+
} else {
248+
map.set(key, [toolCall]);
249+
}
250+
}
251+
return map;
252+
}
253+
254+
function readCurrentWorkflowStep(
255+
steps: AgUiWorkflowStep[],
256+
labels: Record<string, string>,
257+
): string | null {
258+
for (let i = steps.length - 1; i >= 0; i -= 1) {
259+
const step = steps[i];
260+
if (step.status === 'pending') {
261+
return labels[step.name] ?? step.name;
262+
}
263+
}
264+
return null;
265+
}
266+
267+
function readCurrentStatus(
268+
currentWorkflowStep: string | null,
269+
isLoading: boolean,
270+
widgetCount: number,
271+
): string {
272+
if (currentWorkflowStep) {
273+
return currentWorkflowStep;
274+
}
275+
if (isLoading) {
276+
return 'Building travel plan';
277+
}
278+
if (widgetCount > 0) {
279+
return 'Done';
280+
}
281+
return 'Ready';
282+
}
283+
284+
function formatToolArgsValue(args: unknown): string {
285+
if (args === undefined || args === null) {
286+
return '';
287+
}
288+
if (typeof args === 'string') {
289+
return args;
290+
}
291+
try {
292+
return JSON.stringify(args, null, 2);
293+
} catch {
294+
return String(args);
295+
}
296+
}
297+
298+
function readStepLabel(name: string, labels: Record<string, string>): string {
299+
return labels[name] ?? name;
300+
}

0 commit comments

Comments
 (0)