Add Prompt AI model, API, seeds and UI
Introduce PromptAi support: add model, migration, seeder and controller with full CRUD endpoints, and register routes under /api/prompt-ai. Integrate DB-driven prompts into LineConnection (replace hardcoded prompt strings for DPELP and ENV with fetched PromptAi records). Update frontend ModalConfig to add a Prompt AI management tab (fetch, create, edit prompts), plus related UI tweaks (tabs, prompt editor modal, axios/notifications). This makes AI prompts editable at runtime without code changes.
This commit is contained in:
parent
ef20557635
commit
1ae7550d77
|
|
@ -0,0 +1,135 @@
|
|||
import PromptAi from '#models/prompt_ai'
|
||||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
|
||||
export default class PromptAisController {
|
||||
/**
|
||||
* Display a list of all prompt AIs
|
||||
*/
|
||||
async get({}: HttpContext) {
|
||||
const promptAis = await PromptAi.all()
|
||||
return { status: true, data: promptAis }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prompt AI by type
|
||||
*/
|
||||
async getByType({ request, response }: HttpContext) {
|
||||
try {
|
||||
const { type } = request.only(['type'])
|
||||
if (!type) {
|
||||
return response.badRequest({ status: false, message: 'Type is required' })
|
||||
}
|
||||
|
||||
const promptAis = await PromptAi.query().where('type', type).where('is_active', true)
|
||||
return { status: true, data: promptAis }
|
||||
} catch (error) {
|
||||
return response.badRequest({
|
||||
error: error,
|
||||
message: 'Failed to get prompt AI by type',
|
||||
status: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new prompt AI
|
||||
*/
|
||||
async create({ request, response }: HttpContext) {
|
||||
let payload = request.only(['title', 'content', 'description', 'type', 'is_active'])
|
||||
try {
|
||||
// Check if title already exists
|
||||
const existedPromptAi = await PromptAi.findBy('title', payload.title)
|
||||
if (existedPromptAi) {
|
||||
return response.badRequest({
|
||||
status: false,
|
||||
message: 'Prompt AI with this title already exists',
|
||||
})
|
||||
}
|
||||
|
||||
const promptAi = await PromptAi.create({
|
||||
title: payload.title,
|
||||
content: payload.content,
|
||||
description: payload.description || null,
|
||||
type: payload.type || 'general',
|
||||
is_active: payload.is_active !== undefined ? payload.is_active : true,
|
||||
})
|
||||
return response.created({
|
||||
status: true,
|
||||
message: 'Prompt AI created successfully',
|
||||
data: promptAi,
|
||||
})
|
||||
} catch (error) {
|
||||
return response.badRequest({
|
||||
error: error,
|
||||
message: 'Prompt AI create failed',
|
||||
status: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update prompt AI
|
||||
*/
|
||||
async update({ request, response }: HttpContext) {
|
||||
let payload = request.only(['id', 'title', 'content', 'description', 'type', 'is_active'])
|
||||
try {
|
||||
const promptAi = await PromptAi.find(payload.id)
|
||||
if (!promptAi) {
|
||||
return response.status(404).json({ status: false, message: 'Prompt AI not found' })
|
||||
}
|
||||
|
||||
// Check if title already exists (and it's not the same record)
|
||||
if (payload.title && payload.title !== promptAi.title) {
|
||||
const existedPromptAi = await PromptAi.findBy('title', payload.title)
|
||||
if (existedPromptAi) {
|
||||
return response.badRequest({
|
||||
status: false,
|
||||
message: 'Prompt AI with this title already exists',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(promptAi, {
|
||||
title: payload.title || promptAi.title,
|
||||
content: payload.content || promptAi.content,
|
||||
description: payload.description !== undefined ? payload.description : promptAi.description,
|
||||
type: payload.type || promptAi.type,
|
||||
is_active: payload.is_active !== undefined ? payload.is_active : promptAi.is_active,
|
||||
})
|
||||
await promptAi.save()
|
||||
return response.ok({
|
||||
status: true,
|
||||
message: 'Prompt AI updated successfully',
|
||||
data: promptAi,
|
||||
})
|
||||
} catch (error) {
|
||||
return response.badRequest({
|
||||
error: error,
|
||||
message: 'Prompt AI update failed',
|
||||
status: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete prompt AI
|
||||
*/
|
||||
async delete({ request, response }: HttpContext) {
|
||||
try {
|
||||
const { id } = request.only(['id'])
|
||||
const promptAi = await PromptAi.find(id)
|
||||
if (!promptAi) {
|
||||
return response.status(404).json({ status: false, message: 'Prompt AI not found' })
|
||||
}
|
||||
|
||||
await promptAi.delete()
|
||||
return response.ok({ status: true, message: 'Prompt AI deleted successfully' })
|
||||
} catch (error) {
|
||||
return response.badRequest({
|
||||
error: error,
|
||||
message: 'Prompt AI delete failed',
|
||||
status: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { DateTime } from 'luxon'
|
||||
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
||||
|
||||
export default class PromptAi extends BaseModel {
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
|
||||
@column()
|
||||
declare title: string
|
||||
|
||||
@column()
|
||||
declare content: string
|
||||
|
||||
@column()
|
||||
declare description: string | null
|
||||
|
||||
@column()
|
||||
declare type: string
|
||||
|
||||
@column()
|
||||
declare is_active: boolean
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import path from 'node:path'
|
|||
import axios from 'axios'
|
||||
import redis from '@adonisjs/redis/services/main'
|
||||
import Line from '#models/line'
|
||||
import PromptAi from '#models/prompt_ai'
|
||||
import { CustomSocket, ErrorRow, TestResult } from '../ultils/types.js'
|
||||
import momentTZ from 'moment-timezone'
|
||||
import { PhysicalPortTest } from './physical_test_service.js'
|
||||
|
|
@ -882,34 +883,20 @@ export default class LineConnection {
|
|||
*/
|
||||
async detectLogWithAI(log: string) {
|
||||
try {
|
||||
// Get prompt from database
|
||||
const promptRecord = await PromptAi.findBy('type', 'dpelp')
|
||||
if (!promptRecord) {
|
||||
console.log('[ERROR] Prompt DPELP not found in database')
|
||||
return ''
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: 'gpt-4o-mini',
|
||||
max_tokens: 1000,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `You are a network hardware tester.
|
||||
Your task is to analyze router/switch logs to determine whether the device meets hardware standards for reselling.
|
||||
Focus ONLY on hardware-related problems or abnormal warnings.
|
||||
Software or configuration issues (e.g., port up/down, admin down, invalid commands, CLI errors, licensing messages) should be ignored unless they indicate hardware failure.
|
||||
OUTPUT FORMAT (must follow exactly):
|
||||
{
|
||||
"issue": [ "problem 1", "problem 2", ... ],
|
||||
"summary": "short summary under 30 words"
|
||||
}
|
||||
RULES:
|
||||
- Summaries must be in English.
|
||||
- Each issue must be one short line.
|
||||
- If the log contains no hardware issues, output: { "issue": ["No issues detected."], "summary": "No hardware issues found." }
|
||||
- Keep responses concise, readable, and strictly in JSON format.
|
||||
- Do NOT add explanations outside the JSON.
|
||||
- Your job is to detect hardware faults, missing components, overheating, failing modules, PSU issues, sensor anomalies, SIM/card missing, modem errors, transceiver issues, POST/diagnostics failures, etc.
|
||||
The log to analyze will be provided after this prompt.
|
||||
|
||||
Here is the log:
|
||||
|
||||
${log}
|
||||
`,
|
||||
content: promptRecord + "\n Here's the log:\n" + log,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1708,40 +1695,20 @@ Ports Missing/Down: ${missing.length}\n\n`
|
|||
*/
|
||||
async detectShowEnvWithAI(log: string) {
|
||||
try {
|
||||
// Get prompt from database
|
||||
const promptRecord = await PromptAi.findBy('type', 'env')
|
||||
if (!promptRecord) {
|
||||
console.log('[ERROR] Prompt ENV not found in database')
|
||||
return ''
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: 'gpt-4o-mini',
|
||||
max_tokens: 1000,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `You are a network log parser.
|
||||
|
||||
Input is the raw output of Cisco "show environment" or "show environment all".
|
||||
|
||||
Your task:
|
||||
- Focus ONLY on FAN and POWER related information.
|
||||
- Ignore TEMPERATURE, VOLTAGE, and other sensors unless they relate to FAN or POWER.
|
||||
- Extract each FAN or POWER component and its state.
|
||||
- Normalize each item into the format:
|
||||
|
||||
"<NAME>: <STATE>"
|
||||
|
||||
Examples:
|
||||
- "FAN is OK" -> "FAN: OK"
|
||||
- "FAN 2 is FAILED" -> "FAN 2: FAILED"
|
||||
- "POWER SUPPLY A is NOT PRESENT" -> "POWER SUPPLY A: NOT PRESENT"
|
||||
- "PSU 1 Absent" -> "PSU 1: ABSENT"
|
||||
|
||||
Output requirements:
|
||||
- Return ONLY a valid JSON array of strings.
|
||||
- Do NOT include any explanation or extra text.
|
||||
- Do NOT include code block.
|
||||
- JSON must be directly parsable.
|
||||
|
||||
Here is the input log:
|
||||
|
||||
${log}
|
||||
`,
|
||||
content: promptRecord + "\n Here's the log:\n" + log,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'prompt_ais'
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
table.string('title').notNullable().unique()
|
||||
table.text('content').notNullable()
|
||||
table.string('type').notNullable()
|
||||
table.timestamps()
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { BaseSeeder } from '@adonisjs/lucid/seeders'
|
||||
import PromptAiSeeder from './prompt_ai_seeder.js'
|
||||
|
||||
export default class IndexSeeder extends BaseSeeder {
|
||||
async run() {
|
||||
await new PromptAiSeeder().run()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
import { BaseSeeder } from '@adonisjs/lucid/seeders'
|
||||
import PromptAi from '#models/prompt_ai'
|
||||
|
||||
const PROMPT_DPELP = `Bạn là Cisco Network Hardware TAC Engineer.
|
||||
|
||||
Mục tiêu:
|
||||
Phân tích log từ thiết bị Cisco và đưa ra kết luận nhanh, chính xác về tình trạng HARDWARE (trả lời bằng tiếng VIỆT, ngắn gọn, dễ hiểu).
|
||||
|
||||
==================================================
|
||||
PHÂN LOẠI THIẾT BỊ
|
||||
==================
|
||||
|
||||
* Router / ASR / ISR
|
||||
* Switch (Catalyst)
|
||||
* Wireless Controller (WLC 9800)
|
||||
* Access Point (AP)
|
||||
* Catalyst 6500 / 4500 (Special handling)
|
||||
|
||||
==================================================
|
||||
REQUIREMENTS (BẮT BUỘC)
|
||||
=======================
|
||||
|
||||
Router / ASR / WLC:
|
||||
|
||||
* show platform
|
||||
* show environment (hoặc show environment all / show env)
|
||||
* show license (bắt buộc, không dùng show version thay thế)
|
||||
* show inventory
|
||||
* show version
|
||||
|
||||
Switch:
|
||||
|
||||
* show platform
|
||||
* show environment
|
||||
* show inventory
|
||||
* show version
|
||||
* show post → MUST = Passed
|
||||
|
||||
Access Point:
|
||||
|
||||
* show version
|
||||
* show inventory
|
||||
|
||||
Catalyst 6500 / 4500 (EXCEPTION):
|
||||
|
||||
* KHÔNG yêu cầu show platform
|
||||
* BẮT BUỘC:
|
||||
|
||||
* show module
|
||||
* show environment
|
||||
* show inventory
|
||||
* show version
|
||||
* show post (nếu có)
|
||||
|
||||
---
|
||||
|
||||
EXCEPTION (SWITCH ONLY):
|
||||
|
||||
* Nếu technician đã chạy lệnh **show platform** nhưng thiết bị trả về:
|
||||
→ "% Incomplete command" hoặc command không supported
|
||||
→ XEM NHƯ ĐÃ CÓ show platform
|
||||
→ KHÔNG được trả về INSUFFICIENT DATA vì thiếu show platform
|
||||
|
||||
---
|
||||
|
||||
Nếu thiếu lệnh bắt buộc khác:
|
||||
→ RESULT: INSUFFICIENT DATA (missing <command>)
|
||||
→ Ghi rõ thiếu lệnh
|
||||
→ Vẫn được phép nêu dấu hiệu nghi ngờ trong EVIDENCE
|
||||
|
||||
==================================================
|
||||
HARDWARE CHECK RULE
|
||||
===================
|
||||
|
||||
FAIL nếu có:
|
||||
|
||||
* Power Supply Failure / PEM failure
|
||||
* Module / RP / ESP state != ok/active
|
||||
* Fan failure
|
||||
* Temperature warning / critical
|
||||
* Crash / watchdog / hardware error
|
||||
* Environment sensor != Normal
|
||||
* Module status = PwrDown / Failed / Unknown
|
||||
* POST != Passed (đối với switch)
|
||||
* ❗ Chassis authentication failed (platform-level)
|
||||
|
||||
PASS nếu:
|
||||
|
||||
* Tất cả module = ok/active
|
||||
* Environment = Normal
|
||||
* POST = Passed (đối với switch)
|
||||
* Không có lỗi hardware
|
||||
|
||||
PASS WITH WARNING nếu:
|
||||
|
||||
* Không lỗi hardware nhưng có risk:
|
||||
|
||||
* Smart License
|
||||
* Minor issue (AC low, warning logs, etc.)
|
||||
|
||||
⚠️ LƯU Ý:
|
||||
|
||||
* PSU redundancy missing (chỉ có 1 PSU hoặc PSU thứ 2 not present)
|
||||
→ KHÔNG được đưa vào WARNING
|
||||
→ Vẫn có thể là PASS nếu không có lỗi khác
|
||||
|
||||
==================================================
|
||||
SPECIAL LOGIC: CHASSIS AUTHENTICATION
|
||||
=====================================
|
||||
|
||||
* Nếu phát hiện log:
|
||||
→ "chassis authentication failed"
|
||||
→ "PLATFORM_SCC-1-AUTHENTICATION_FAIL"
|
||||
|
||||
→ PHẢI kết luận FAIL ngay (hardware/security platform issue)
|
||||
|
||||
EXCEPTION:
|
||||
|
||||
* Nếu log liên quan đến:
|
||||
→ login failed
|
||||
→ authentication failed do username/password (AAA, TACACS, RADIUS)
|
||||
|
||||
→ KHÔNG tính là hardware issue
|
||||
|
||||
==================================================
|
||||
PSU LOGIC (QUAN TRỌNG)
|
||||
======================
|
||||
|
||||
* PSU fail nhưng:
|
||||
Vin = 0V AND Iout = 0A
|
||||
→ KHÔNG lỗi (chưa cắm điện)
|
||||
|
||||
* PSU có điện nhưng fail → FAIL
|
||||
|
||||
* Có log "Power Supply Failure" → FAIL
|
||||
|
||||
* Chỉ có 1 PSU → KHÔNG cảnh báo (no warning)
|
||||
|
||||
* AC low → PASS WITH WARNING
|
||||
|
||||
==================================================
|
||||
LICENSE RULE
|
||||
============
|
||||
|
||||
Switch:
|
||||
|
||||
* Có thể lấy từ show version
|
||||
|
||||
Router / ASR:
|
||||
|
||||
* Bắt buộc show license
|
||||
|
||||
Phân loại:
|
||||
|
||||
* Traditional → OK
|
||||
* Smart License → PASS WITH WARNING
|
||||
|
||||
==================================================
|
||||
PORT ANALYSIS
|
||||
=============
|
||||
|
||||
Phải trích xuất:
|
||||
|
||||
* Total ports
|
||||
* RJ45 ports
|
||||
* PoE ports
|
||||
* SFP/Uplink ports
|
||||
* Uplink module
|
||||
|
||||
==================================================
|
||||
MODULE ANALYSIS (6500/4500)
|
||||
===========================
|
||||
|
||||
* Phải liệt kê từng module:
|
||||
|
||||
* Slot number
|
||||
* Model
|
||||
* Serial
|
||||
* Status: PASS / FAIL / NOT APPLICABLE
|
||||
|
||||
* Nếu:
|
||||
|
||||
* Status = Ok + Online Diag = Pass → PASS
|
||||
* Status = PwrDown / Failed → FAIL
|
||||
* Online Diag = Not Applicable → NOT APPLICABLE
|
||||
|
||||
⚠️ CRITICAL REQUIREMENT:
|
||||
|
||||
* "show module" là BẮT BUỘC cho Catalyst 6500 / 4500
|
||||
|
||||
* Nếu thiếu "show module":
|
||||
|
||||
→ RESULT: INSUFFICIENT DATA (missing show module)
|
||||
→ KHÔNG được phép trả về PASS / FAIL / PASS WITH WARNING
|
||||
→ KHÔNG được suy luận trạng thái module từ "show inventory"
|
||||
|
||||
* Rule này có độ ưu tiên CAO NHẤT
|
||||
→ Override toàn bộ logic hardware khác
|
||||
|
||||
==================================================
|
||||
MISSING LOGIC (GLOBAL)
|
||||
======================
|
||||
|
||||
* Đối với Catalyst 6500 / 4500:
|
||||
|
||||
* Nếu thiếu "show module"
|
||||
→ LUÔN trả về:
|
||||
RESULT: INSUFFICIENT DATA (missing show module)
|
||||
|
||||
* KHÔNG phụ thuộc vào:
|
||||
|
||||
* show environment
|
||||
* show version
|
||||
* show inventory
|
||||
* bất kỳ log nào khác
|
||||
|
||||
==================================================
|
||||
OUTPUT FORMAT (STRICT)
|
||||
======================
|
||||
|
||||
RESULT: PASS / PASS WITH WARNING / FAIL / INSUFFICIENT DATA
|
||||
(Phải kèm lý do ngắn gọn trong ngoặc)
|
||||
|
||||
SUMMARY:
|
||||
|
||||
* Model:
|
||||
* Serial:
|
||||
* Hardware status:
|
||||
* Key issue:
|
||||
* Module <slot> (<model>, SN: <serial>): <PASS/FAIL>
|
||||
|
||||
PORT DETAILS:
|
||||
|
||||
* Total ports:
|
||||
* RJ45 ports:
|
||||
* PoE ports:
|
||||
* SFP/Uplink ports:
|
||||
* Uplink module:
|
||||
|
||||
LICENSE:
|
||||
|
||||
* Type:
|
||||
* Status:
|
||||
|
||||
WARNING:
|
||||
|
||||
* (nếu có, KHÔNG bao gồm PSU redundancy missing)
|
||||
|
||||
MISSING:
|
||||
|
||||
* (nếu thiếu)
|
||||
|
||||
EVIDENCE:
|
||||
|
||||
* tối đa 3 dòng
|
||||
|
||||
RECOMMENDATION:
|
||||
|
||||
* hành động
|
||||
|
||||
MODULE STATUS (nếu là 6500/4500):
|
||||
|
||||
* Module <slot> (<model>, SN: <serial>): PASS / FAIL / NOT APPLICABLE
|
||||
|
||||
==================================================
|
||||
OUTPUT DELIVERY
|
||||
===============
|
||||
|
||||
* LUÔN dùng writing block
|
||||
* Không thêm giải thích ngoài report
|
||||
* Format sạch, copy được ngay
|
||||
* Ngôn ngữ: TIẾNG VIỆT
|
||||
`
|
||||
|
||||
const PROMPT_ENV = `You are a network log parser.
|
||||
Input is the raw output of Cisco "show environment" or "show environment all".
|
||||
|
||||
Your task:
|
||||
- Focus ONLY on FAN and POWER related information.
|
||||
- Ignore TEMPERATURE, VOLTAGE, and other sensors unless they relate to FAN or POWER.
|
||||
- Extract each FAN or POWER component and its state.
|
||||
- Normalize each item into the format:
|
||||
|
||||
"<NAME>: <STATE>"
|
||||
|
||||
Examples:
|
||||
- "FAN is OK" -> "FAN: OK"
|
||||
- "FAN 2 is FAILED" -> "FAN 2: FAILED"
|
||||
- "POWER SUPPLY A is NOT PRESENT" -> "POWER SUPPLY A: NOT PRESENT"
|
||||
- "PSU 1 Absent" -> "PSU 1: ABSENT"
|
||||
|
||||
Output requirements:
|
||||
- Return ONLY a valid JSON array of strings.
|
||||
- Do NOT include any explanation or extra text.
|
||||
- Do NOT include code block.
|
||||
- JSON must be directly parsable.
|
||||
`
|
||||
|
||||
export default class extends BaseSeeder {
|
||||
async run() {
|
||||
// Check if data already exists to avoid duplicates
|
||||
const existingDpelp = await PromptAi.findBy('title', 'Prompt ran after done DPELP')
|
||||
const existingEnv = await PromptAi.findBy('title', 'Run check log off show')
|
||||
|
||||
if (!existingDpelp) {
|
||||
await PromptAi.create({
|
||||
title: 'Prompt ran after done DPELP',
|
||||
type: 'dpelp',
|
||||
content: PROMPT_DPELP,
|
||||
})
|
||||
console.log('✅ Created prompt: Prompt ran after done DPELP')
|
||||
}
|
||||
|
||||
if (!existingEnv) {
|
||||
await PromptAi.create({
|
||||
title: 'Run check log off show environment',
|
||||
type: 'env',
|
||||
content: PROMPT_ENV,
|
||||
})
|
||||
console.log('✅ Created prompt: Run check log off show')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,3 +130,13 @@ router
|
|||
router.post('/delete', '#controllers/keywords_controller.delete')
|
||||
})
|
||||
.prefix('/api/keywords')
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/', '#controllers/prompt_ais_controller.get')
|
||||
router.post('/getByType', '#controllers/prompt_ais_controller.getByType')
|
||||
router.post('/create', '#controllers/prompt_ais_controller.create')
|
||||
router.post('/update', '#controllers/prompt_ais_controller.update')
|
||||
router.post('/delete', '#controllers/prompt_ais_controller.delete')
|
||||
})
|
||||
.prefix('/api/prompt-ai')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { Modal, Button, ColorPicker, Group, Grid } from "@mantine/core";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
ColorPicker,
|
||||
Group,
|
||||
Grid,
|
||||
Tabs,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Table,
|
||||
Badge,
|
||||
ActionIcon,
|
||||
Stack,
|
||||
} from "@mantine/core";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import axios from "axios";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
interface Props {
|
||||
opened: boolean;
|
||||
|
|
@ -9,9 +26,33 @@ interface Props {
|
|||
onSave: () => void;
|
||||
}
|
||||
|
||||
interface PromptAI {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
title: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||
const [color, setColor] = useState("#41ee4a");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [prompts, setPrompts] = useState<PromptAI[]>([]);
|
||||
const [loadingPrompts, setLoadingPrompts] = useState(false);
|
||||
const [modalEditing, setModalEditing] = useState(false);
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
title: "",
|
||||
type: "general",
|
||||
content: "",
|
||||
});
|
||||
|
||||
const xtermRef = useRef<HTMLDivElement>(null);
|
||||
const terminal = useRef<Terminal>(null);
|
||||
const fitRef = useRef<FitAddon>(null);
|
||||
|
|
@ -22,6 +63,86 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
|||
if (saved) setColor(saved);
|
||||
}, []);
|
||||
|
||||
// Fetch all prompts
|
||||
const fetchPrompts = async () => {
|
||||
setLoadingPrompts(true);
|
||||
try {
|
||||
const response = await axios.get(apiUrl + "api/prompt-ai");
|
||||
if (response.data.status) {
|
||||
setPrompts(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching prompts:", error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Failed to load prompts",
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setLoadingPrompts(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Submit form (create or update)
|
||||
const handleSubmitPrompt = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.title.trim() || !formData.content.trim()) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Title and Content are required",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsDisabled(true);
|
||||
try {
|
||||
const url = editingId ? "/api/prompt-ai/update" : "/api/prompt-ai/create";
|
||||
const payload = editingId ? { id: editingId, ...formData } : formData;
|
||||
|
||||
const response = await axios.post(apiUrl + url, payload);
|
||||
if (response?.data?.status) {
|
||||
notifications.show({
|
||||
title: "Success",
|
||||
message: "Prompt updated successfully",
|
||||
color: "green",
|
||||
});
|
||||
setFormData({
|
||||
title: "",
|
||||
type: "general",
|
||||
content: "",
|
||||
});
|
||||
setEditingId(null);
|
||||
fetchPrompts();
|
||||
setIsDisabled(false);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: response?.data?.message || "Operation failed",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setIsDisabled(false);
|
||||
console.error("Error:", error);
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "An error occurred",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Edit prompt
|
||||
const handleEditPrompt = (prompt: PromptAI) => {
|
||||
setFormData({
|
||||
title: prompt.title,
|
||||
type: prompt.type,
|
||||
content: prompt.content,
|
||||
});
|
||||
setEditingId(prompt.id);
|
||||
setModalEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
localStorage.setItem("terminal-text-color", color);
|
||||
onClose();
|
||||
|
|
@ -53,7 +174,7 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
|||
if (xtermRef.current) terminal.current.open(xtermRef.current);
|
||||
|
||||
terminal.current?.write(
|
||||
"Change color \nChange color\nChange color\nChange color"
|
||||
"Change color \nChange color\nChange color\nChange color",
|
||||
);
|
||||
fitAddon.fit();
|
||||
}, [loaded, xtermRef]);
|
||||
|
|
@ -72,6 +193,7 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
|||
if (opened) {
|
||||
setTimeout(() => {
|
||||
setLoaded(true);
|
||||
fetchPrompts();
|
||||
}, 100);
|
||||
} else {
|
||||
setLoaded(false);
|
||||
|
|
@ -84,78 +206,192 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
size={"xl"}
|
||||
size={"lg"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="Terminal Text Color"
|
||||
title="Configuration"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<Group>
|
||||
<ColorPicker
|
||||
format="hex"
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
fullWidth
|
||||
withPicker={false}
|
||||
swatches={[
|
||||
"#ffffff",
|
||||
"#41ee4a",
|
||||
"#fa5252",
|
||||
"#e64980",
|
||||
"#be4bdb",
|
||||
"#7950f2",
|
||||
"#4c6ef5",
|
||||
"#228be6",
|
||||
"#15aabf",
|
||||
"#12b886",
|
||||
"#40c057",
|
||||
"#82c91e",
|
||||
"#fab005",
|
||||
"#fd7e14",
|
||||
]}
|
||||
<Tabs defaultValue="color">
|
||||
<Tabs.List justify="center">
|
||||
<Tabs.Tab value="color">Terminal Color</Tabs.Tab>
|
||||
<Tabs.Tab value="prompt">Prompt AI</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="color">
|
||||
<Grid style={{ height: "300px" }}>
|
||||
<Grid.Col span={6}>
|
||||
<Group>
|
||||
<ColorPicker
|
||||
format="hex"
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
fullWidth
|
||||
withPicker={false}
|
||||
swatches={[
|
||||
"#ffffff",
|
||||
"#41ee4a",
|
||||
"#fa5252",
|
||||
"#e64980",
|
||||
"#be4bdb",
|
||||
"#7950f2",
|
||||
"#4c6ef5",
|
||||
"#228be6",
|
||||
"#15aabf",
|
||||
"#12b886",
|
||||
"#40c057",
|
||||
"#82c91e",
|
||||
"#fab005",
|
||||
"#fd7e14",
|
||||
]}
|
||||
/>
|
||||
|
||||
<Button fullWidth onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
paddingBottom: "4px",
|
||||
maxHeight: "220px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
ref={xtermRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingLeft: "10px",
|
||||
paddingBottom: "10px",
|
||||
fontSize: "12px",
|
||||
maxHeight: "220px",
|
||||
height: "220px",
|
||||
padding: "4px",
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel style={{ height: "300px" }} value="prompt">
|
||||
<Stack gap="md">
|
||||
{/* Table */}
|
||||
<div>
|
||||
<h4 style={{ marginBottom: "10px" }}>Existing Prompts</h4>
|
||||
{loadingPrompts ? (
|
||||
<div>Loading...</div>
|
||||
) : prompts.length === 0 ? (
|
||||
<div>No prompts found</div>
|
||||
) : (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Title</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{prompts.map((prompt) => (
|
||||
<Table.Tr key={prompt.id}>
|
||||
<Table.Td
|
||||
style={{
|
||||
maxWidth: "200px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{prompt.title}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm">{prompt.type}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={0}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
onClick={() => handleEditPrompt(prompt)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
{/* <ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => handleDeletePrompt(prompt.id)}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon> */}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Modal
|
||||
size={"xl"}
|
||||
style={{ position: "absolute", left: 0 }}
|
||||
opened={modalEditing}
|
||||
onClose={() => setModalEditing(false)}
|
||||
title="Edit Prompt AI"
|
||||
>
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmitPrompt}>
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Title"
|
||||
placeholder="e.g., Prompt ran after done DPELP"
|
||||
value={formData.title}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, title: e.currentTarget.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button fullWidth onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
paddingBottom: "4px",
|
||||
maxHeight: "220px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
ref={xtermRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingLeft: "10px",
|
||||
paddingBottom: "10px",
|
||||
fontSize: "12px",
|
||||
maxHeight: "220px",
|
||||
height: "220px",
|
||||
padding: "4px",
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
<Textarea
|
||||
label="Content"
|
||||
placeholder="Prompt content"
|
||||
value={formData.content}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, content: e.currentTarget.value })
|
||||
}
|
||||
rows={20}
|
||||
required
|
||||
resize="vertical"
|
||||
/>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button disabled={isDisabled} type="submit">
|
||||
{editingId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue