fix: restrict NAV2 prebuilt artifacts to amd64

This commit is contained in:
2026-08-11 14:21:21 +09:00
parent 196ba51ec6
commit 324e644ef6
9 changed files with 109 additions and 22 deletions
@@ -0,0 +1,13 @@
UPDATE "course_week_modules"
SET "prebuilt_artifacts" = jsonb_build_array(
jsonb_build_object(
'type', 'build-install',
'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst',
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst',
'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9',
'architecture', 'amd64',
'required', false,
'extractPath', '.'
)
)
WHERE "folder_name" = '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws';
@@ -0,0 +1,18 @@
UPDATE course_week_modules AS module
SET prebuilt_artifacts = (
SELECT jsonb_agg(
CASE
WHEN artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
THEN jsonb_set(artifact, '{architecture}', '"amd64"'::jsonb, true)
ELSE artifact
END
)
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
)
WHERE EXISTS (
SELECT 1
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
WHERE artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
);
+9 -9
View File
@@ -153,23 +153,23 @@ WHERE w.course_id IN (SELECT id FROM target_course) AND w.week = 2;
WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS ( WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS (
VALUES VALUES
('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git', ('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git', ('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git', ('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git', ('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git', ('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'),
('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git', ('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git', ('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git', ('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'), jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git', ('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '8b36d3fda6a81a49da140529d9874e99b734d05e01e12a49e719e2b26f8803d9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin') jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
) )
INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image) INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image)
SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image
+3 -1
View File
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Query, Req } from '@nestjs/
import { Request } from 'express'; import { Request } from 'express';
import { ChatService } from './chat.service'; import { ChatService } from './chat.service';
import { CancelActiveAiResponseDto } from './dto/cancel-active-ai-response.dto';
import { CreateAiResponseDto } from './dto/create-ai-response.dto'; import { CreateAiResponseDto } from './dto/create-ai-response.dto';
import { CreateChatMessageDto } from './dto/create-chat-message.dto'; import { CreateChatMessageDto } from './dto/create-chat-message.dto';
import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto'; import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto';
@@ -64,10 +65,11 @@ export class ChatController {
@Post('ai/cancel-active') @Post('ai/cancel-active')
cancelActiveAiResponse( cancelActiveAiResponse(
@Req() request: Request, @Req() request: Request,
@Body() body: { week?: number }, @Body() body: CancelActiveAiResponseDto,
) { ) {
return this.chatService.cancelActiveAiResponse( return this.chatService.cancelActiveAiResponse(
this.getBearerToken(request), this.getBearerToken(request),
body.courseId,
body.week, body.week,
); );
} }
+41 -9
View File
@@ -57,6 +57,7 @@ type FutureDriveLlmChatResponse = {
const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20; const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20;
const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6; const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6;
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide';
@Injectable() @Injectable()
export class ChatService { export class ChatService {
@@ -79,7 +80,7 @@ export class ChatService {
async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) { async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
const student = await this.getLmsUser(accessToken); const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getStudentCourse(student.id, query.courseId);
const week = query.week const week = query.week
? await this.getCourseWeek(course.id, query.week) ? await this.getCourseWeek(course.id, query.week)
: null; : null;
@@ -122,7 +123,7 @@ export class ChatService {
async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) { async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
const student = await this.getLmsUser(accessToken); const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getStudentCourse(student.id, query.courseId);
const week = query.week const week = query.week
? await this.getCourseWeek(course.id, query.week) ? await this.getCourseWeek(course.id, query.week)
: null; : null;
@@ -167,7 +168,7 @@ export class ChatService {
dto: CreateChatMessageDto, dto: CreateChatMessageDto,
) { ) {
const student = await this.getLmsUser(accessToken); const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getStudentCourse(student.id, dto.courseId);
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null; const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
const thread = await this.getOrCreateThread({ const thread = await this.getOrCreateThread({
courseId: course.id, courseId: course.id,
@@ -220,7 +221,7 @@ export class ChatService {
dto: CreateAiResponseDto, dto: CreateAiResponseDto,
) { ) {
const student = await this.getLmsUser(accessToken); const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getStudentCourse(student.id, dto.courseId);
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null; const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
const thread = await this.getOrCreateThread({ const thread = await this.getOrCreateThread({
courseId: course.id, courseId: course.id,
@@ -425,10 +426,11 @@ export class ChatService {
async cancelActiveAiResponse( async cancelActiveAiResponse(
accessToken: string | undefined, accessToken: string | undefined,
courseId: string,
weekNumber?: number, weekNumber?: number,
) { ) {
const student = await this.getLmsUser(accessToken); const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getStudentCourse(student.id, courseId);
const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null; const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null;
const message = await this.prisma.chatMessage.findFirst({ const message = await this.prisma.chatMessage.findFirst({
where: { where: {
@@ -487,7 +489,7 @@ export class ChatService {
query: ListChatThreadsDto, query: ListChatThreadsDto,
) { ) {
const instructor = await this.getInstructorLmsUser(accessToken); const instructor = await this.getInstructorLmsUser(accessToken);
const course = await this.getActiveCourse(); const course = await this.getInstructorCourse(instructor.id, query.courseId);
const week = query.week const week = query.week
? await this.getCourseWeek(course.id, query.week) ? await this.getCourseWeek(course.id, query.week)
: null; : null;
@@ -680,11 +682,41 @@ export class ChatService {
}; };
} }
private async getActiveCourse() { private async getStudentCourse(studentId: string, courseId?: string) {
if (!courseId) throw new NotFoundException('Course not found');
const course = await this.prisma.course.findFirst({ const course = await this.prisma.course.findFirst({
where: { slug: 'futuredrive-autonomy-basic', isActive: true }, where: {
id: courseId,
isActive: true,
OR: [
{ slug: PUBLIC_GUIDE_COURSE_SLUG },
{
enrollments: {
some: {
userId: studentId,
status: 'ACTIVE',
},
},
},
],
},
}); });
if (!course) throw new NotFoundException('Curriculum course not found'); if (!course) throw new NotFoundException('Course not found');
return course;
}
private async getInstructorCourse(instructorId: string, courseId?: string) {
if (!courseId) throw new NotFoundException('Course not found');
const course = await this.prisma.course.findFirst({
where: {
id: courseId,
isActive: true,
teacherId: instructorId,
},
});
if (!course) throw new NotFoundException('Course not found');
return course; return course;
} }
@@ -0,0 +1,12 @@
import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
export class CancelActiveAiResponseDto {
@IsUUID()
courseId: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
week?: number;
}
+4 -1
View File
@@ -1,6 +1,9 @@
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; import { IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
export class CreateAiResponseDto { export class CreateAiResponseDto {
@IsUUID()
courseId: string;
@IsString() @IsString()
content: string; content: string;
+4 -1
View File
@@ -1,6 +1,9 @@
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; import { IsIn, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
export class CreateChatMessageDto { export class CreateChatMessageDto {
@IsUUID()
courseId: string;
@IsIn(['ai', 'teacher']) @IsIn(['ai', 'teacher'])
mode: 'ai' | 'teacher'; mode: 'ai' | 'teacher';
+5 -1
View File
@@ -1,7 +1,11 @@
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsIn, IsInt, IsOptional, Max, Min } from 'class-validator'; import { IsBoolean, IsIn, IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
export class ListChatThreadsDto { export class ListChatThreadsDto {
@IsOptional()
@IsUUID()
courseId?: string;
@IsOptional() @IsOptional()
@IsIn(['ai', 'teacher']) @IsIn(['ai', 'teacher'])
mode?: 'ai' | 'teacher'; mode?: 'ai' | 'teacher';