Skip to content
Open
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
13 changes: 5 additions & 8 deletions blotztask-api/Modules/ChatTaskGenerator/AiHubFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

namespace BlotzTask.Modules.ChatTaskGenerator;

// All methods on this hub use ReceiveGenerationResult as the error envelope. If a new method
// with a different client contract is added, handle it before reaching this filter.
// All hub method errors are sent to the caller via ReceiveGenerationError. If a new hub method
// needs a different error contract, handle it before reaching this filter.
public class AiHubFilter(ILogger<AiHubFilter> logger) : IHubFilter
{
public async ValueTask<object?> InvokeMethodAsync(
Expand All @@ -21,9 +21,8 @@ public class AiHubFilter(ILogger<AiHubFilter> logger) : IHubFilter
{
logger.LogWarning(ex, "Hub method {Method} failed. ErrorCode: {Code}",
invocationContext.HubMethodName, ex.Code);
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationResult", new AiGenerateMessage
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationError", new AiGenerationError
{
IsSuccess = false,
ErrorCode = ex.Code.ToString(),
ErrorMessage = ex.Message
});
Expand All @@ -32,9 +31,8 @@ public class AiHubFilter(ILogger<AiHubFilter> logger) : IHubFilter
catch (AiQuotaExceededException ex)
{
logger.LogWarning(ex, "Hub method {Method} failed: quota exceeded", invocationContext.HubMethodName);
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationResult", new AiGenerateMessage
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationError", new AiGenerationError
{
IsSuccess = false,
ErrorCode = AiErrorCode.QuotaExceeded.ToString(),
ErrorMessage = ex.Message
});
Expand All @@ -44,9 +42,8 @@ public class AiHubFilter(ILogger<AiHubFilter> logger) : IHubFilter
{
logger.LogError(ex, "Hub method {Method} failed unexpectedly. ConnectionId: {ConnectionId}",
invocationContext.HubMethodName, invocationContext.Context.ConnectionId);
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationResult", new AiGenerateMessage
await invocationContext.Hub.Clients.Caller.SendAsync("ReceiveGenerationError", new AiGenerationError
{
IsSuccess = false,
ErrorCode = AiErrorCode.Unknown.ToString(),
ErrorMessage = "An unexpected error occurred. Please try again."
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ namespace BlotzTask.Modules.ChatTaskGenerator.Dtos;
/// </summary>
public class AiGenerateMessage
{
[JsonPropertyName("isSuccess")]
[Description("Indicates whether extraction was successful. True if at least one task or one note was extracted, false otherwise.")]
public bool IsSuccess { get; set; }

[JsonPropertyName("userInput")]
public string UserInput { get; set; } = "";
Expand All @@ -24,14 +21,7 @@ public class AiGenerateMessage
[JsonPropertyName("extractedNotes")]
[Description("Array of notes extracted from user input (items with no date/time). Empty when all have time.")]
public List<ExtractedNote> ExtractedNotes { get; set; } = new();

[JsonPropertyName("errorCode")]
[Description("Machine-readable error code identifying the failure type. Empty string when isSuccess is true.")]
public string ErrorCode { get; set; } = "";

[JsonPropertyName("errorMessage")]
[Description("Error message explaining why task extraction failed. Empty string when isSuccess is true.")]
public string ErrorMessage { get; set; } = "";


[JsonPropertyName("inputTokens")]
[Description("Number of prompt tokens used.")]
Expand Down
12 changes: 12 additions & 0 deletions blotztask-api/Modules/ChatTaskGenerator/Dtos/AiGenerationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace BlotzTask.Modules.ChatTaskGenerator.Dtos;

public class AiGenerationError
{
[JsonPropertyName("errorCode")]
public string ErrorCode { get; set; } = "";

[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; } = "";
}
34 changes: 17 additions & 17 deletions blotztask-api/Modules/ChatTaskGenerator/Services/AiChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public async Task<AiGenerateMessage> GenerateAiResponse(
{
context.Tools.ResetCallCount();

int inputTokens = 0, outputTokens = 0, totalTokens = 0;

try
{
await checkAiQuotaService.CheckQuotaAsync(userId, ct);
Expand All @@ -82,9 +84,9 @@ public async Task<AiGenerateMessage> GenerateAiResponse(
var response = await context.Agent.RunAsync(userMessage, context.Session, cancellationToken: ct);
runSw.Stop();

int inputTokens = (int)(response.Usage?.InputTokenCount ?? 0);
int outputTokens = (int)(response.Usage?.OutputTokenCount ?? 0);
int totalTokens = (int)(response.Usage?.TotalTokenCount ?? 0);
inputTokens = (int)(response.Usage?.InputTokenCount ?? 0);
outputTokens = (int)(response.Usage?.OutputTokenCount ?? 0);
totalTokens = (int)(response.Usage?.TotalTokenCount ?? 0);
await recordAiUsageService.RecordAiUsageAsync(new RecordAiUsageRequest
{
UserId = userId,
Expand All @@ -97,20 +99,6 @@ await recordAiUsageService.RecordAiUsageAsync(new RecordAiUsageRequest
"TaskGeneration: RunAsync completed in {RunMs}ms | InputTokens={InputTokens} | OutputTokens={OutputTokens} | TotalTokens={TotalTokens} | ToolCalls={ToolCallCount} | Tasks={TaskCount} | Notes={NoteCount}",
runSw.ElapsedMilliseconds, inputTokens, outputTokens, totalTokens,
context.Tools.ToolCallCount, context.Tools.Tasks.Count, context.Tools.Notes.Count);

var isSuccess = context.Tools.ToolCallCount > 0;

return new AiGenerateMessage
{
IsSuccess = isSuccess,
ErrorCode = isSuccess ? "" : AiErrorCode.NoTasksExtracted.ToString(),
ExtractedTasks = context.Tools.Tasks,
ExtractedNotes = context.Tools.Notes,
ErrorMessage = isSuccess ? "" : "Could not extract any tasks or notes from your input.",
InputTokens = inputTokens,
OutputTokens = outputTokens,
TotalTokens = totalTokens
};
}
catch (OperationCanceledException oce)
{
Expand All @@ -137,5 +125,17 @@ await recordAiUsageService.RecordAiUsageAsync(new RecordAiUsageRequest
throw new AiTaskGenerationException(AiErrorCode.Unknown,
"An unhandled exception occurred during AI task generation.", ex);
}

if (context.Tools.ToolCallCount <= 0)
throw new AiTaskGenerationException(AiErrorCode.NoTasksExtracted, "No tasks or notes were extracted.");

return new AiGenerateMessage
{
ExtractedTasks = context.Tools.Tasks,
ExtractedNotes = context.Tools.Notes,
InputTokens = inputTokens,
OutputTokens = outputTokens,
TotalTokens = totalTokens
};
}
}
30 changes: 15 additions & 15 deletions blotztask-mobile/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import * as signalR from "@microsoft/signalr";
import { File as ExpoFile } from "expo-file-system";
import { signalRService } from "@/feature/ai-task-generate/services/ai-task-generator-signalr-service";
import { AiNoteDTO, AiResultMessageDTO, ExtractedTaskDTO } from "../models/ai-result-message-dto";
import {
AiGenerationErrorDTO,
AiNoteDTO,
AiResultMessageDTO,
ExtractedTaskDTO,
} from "../models/ai-result-message-dto";
import { useTranslation } from "react-i18next";
import Toast from "react-native-toast-message";

Expand Down Expand Up @@ -68,20 +73,23 @@
setIsAiGenerating(false);
requestStartedAtRef.current = null;

if (!result.isSuccess) {
setStreamedTasks([]);
setStreamedNotes([]);
const i18nKey = ERROR_CODE_TO_I18N_KEY[result.errorCode ?? ""] ?? "errors.default";
Toast.show({ type: "error", text1: t(i18nKey) });
return;
}

// Sync to authoritative final list — streaming only covers CreateTask, not RemoveTask/UpdateTask
setStreamedTasks(result.extractedTasks ?? []);
setStreamedNotes(result.extractedNotes ?? []);
setUserInput(result.userInput);
};

const generationErrorHandler = (error: AiGenerationErrorDTO) => {
setTranscript(undefined);
setIsAiGenerating(false);
requestStartedAtRef.current = null;
setStreamedTasks([]);
setStreamedNotes([]);

const i18nKey = ERROR_CODE_TO_I18N_KEY[error.errorCode] ?? "errors.default";
Toast.show({ type: "error", text1: t(i18nKey) });
};

useEffect(() => {
let conn: signalR.HubConnection | null = null;

Expand All @@ -91,6 +99,7 @@
conn = newConn;
setConnection(conn);
conn.on("ReceiveGenerationResult", generationCompleteHandler);
conn.on("ReceiveGenerationError", generationErrorHandler);
conn.on("ReceiveTranscript", (text: string) => setTranscript(text));
conn.on("ReceiveTaskExtracted", (task: ExtractedTaskDTO) => {
if (requestStartedAtRef.current == null) return;
Expand All @@ -107,13 +116,14 @@
return () => {
if (conn) {
conn.off("ReceiveGenerationResult", generationCompleteHandler);
conn.off("ReceiveGenerationError", generationErrorHandler);
conn.off("ReceiveTranscript");
conn.off("ReceiveTaskExtracted");
conn.off("ReceiveNoteExtracted");
conn.stop().catch((error) => console.error("Error stopping SignalR connection:", error));
}
};
}, []);

Check warning on line 126 in blotztask-mobile/src/feature/ai-task-generate/hooks/useAiTaskGenerator.ts

View workflow job for this annotation

GitHub Actions / build-and-test-mobile-frontend

React Hook useEffect has missing dependencies: 'generationCompleteHandler' and 'generationErrorHandler'. Either include them or remove the dependency array

return {
userInput,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export interface AiResultMessageDTO {
isSuccess: boolean;
userInput?: string;
extractedTasks?: ExtractedTaskDTO[];
extractedNotes?: AiNoteDTO[];
errorCode?: string;
errorMessage?: string;
}

export interface AiGenerationErrorDTO {
errorCode: string;
errorMessage: string;
}

export interface AiNoteDTO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useTaskMutations from "@/shared/hooks/useTaskMutations";
import { useNotesMutation } from "@/feature/notes/hooks/useNotesMutation";
import Toast from "react-native-toast-message";
import { analytics } from "@/shared/services/analytics";
import { toastConfig } from "@/shared/components/toast-config";

export default function AiTaskSheetScreen() {
// --- Hooks ---
Expand All @@ -31,7 +32,14 @@ export default function AiTaskSheetScreen() {
const [textInput, setTextInput] = useState("");
const { isVisible: isKeyboardVisible } = useKeyboardState();
const { isHoldHintVisible, showHoldHint, hideHoldHint } = useHoldHint(1500);
const { userInput, transcript, streamedTasks, streamedNotes, submitAudioForTranscription, sendTextMessage } = useAiTaskGenerator({
const {
userInput,
transcript,
streamedTasks,
streamedNotes,
submitAudioForTranscription,
sendTextMessage,
} = useAiTaskGenerator({
setIsAiGenerating,
});
const { labels } = useAllLabels();
Expand Down Expand Up @@ -168,7 +176,6 @@ export default function AiTaskSheetScreen() {
)}

{/* Input bar sticks to the keyboard only */}

<AiInputBar
// Text input
textInput={textInput}
Expand All @@ -190,6 +197,7 @@ export default function AiTaskSheetScreen() {
</KeyboardStickyView>
</LinearGradient>
</View>
<Toast config={toastConfig} position="bottom" bottomOffset={120} />
Comment thread
nxn-nicole marked this conversation as resolved.
</View>
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can revert package-lock json?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you changed the package-lock json code but you didn't change any code in package json

}
Loading