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 axios from 'axios'
|
||||||
import redis from '@adonisjs/redis/services/main'
|
import redis from '@adonisjs/redis/services/main'
|
||||||
import Line from '#models/line'
|
import Line from '#models/line'
|
||||||
|
import PromptAi from '#models/prompt_ai'
|
||||||
import { CustomSocket, ErrorRow, TestResult } from '../ultils/types.js'
|
import { CustomSocket, ErrorRow, TestResult } from '../ultils/types.js'
|
||||||
import momentTZ from 'moment-timezone'
|
import momentTZ from 'moment-timezone'
|
||||||
import { PhysicalPortTest } from './physical_test_service.js'
|
import { PhysicalPortTest } from './physical_test_service.js'
|
||||||
|
|
@ -882,34 +883,20 @@ export default class LineConnection {
|
||||||
*/
|
*/
|
||||||
async detectLogWithAI(log: string) {
|
async detectLogWithAI(log: string) {
|
||||||
try {
|
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 = {
|
const payload = {
|
||||||
model: 'gpt-4o-mini',
|
model: 'gpt-4o-mini',
|
||||||
max_tokens: 1000,
|
max_tokens: 1000,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `You are a network hardware tester.
|
content: promptRecord + "\n Here's the log:\n" + log,
|
||||||
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}
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -1708,40 +1695,20 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
*/
|
*/
|
||||||
async detectShowEnvWithAI(log: string) {
|
async detectShowEnvWithAI(log: string) {
|
||||||
try {
|
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 = {
|
const payload = {
|
||||||
model: 'gpt-4o-mini',
|
model: 'gpt-4o-mini',
|
||||||
max_tokens: 1000,
|
max_tokens: 1000,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `You are a network log parser.
|
content: promptRecord + "\n Here's the log:\n" + log,
|
||||||
|
|
||||||
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}
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
router.post('/delete', '#controllers/keywords_controller.delete')
|
||||||
})
|
})
|
||||||
.prefix('/api/keywords')
|
.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 { 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 { Terminal } from "xterm";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
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 {
|
interface Props {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -9,9 +26,33 @@ interface Props {
|
||||||
onSave: () => void;
|
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) {
|
export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||||
const [color, setColor] = useState("#41ee4a");
|
const [color, setColor] = useState("#41ee4a");
|
||||||
const [loaded, setLoaded] = useState(false);
|
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 xtermRef = useRef<HTMLDivElement>(null);
|
||||||
const terminal = useRef<Terminal>(null);
|
const terminal = useRef<Terminal>(null);
|
||||||
const fitRef = useRef<FitAddon>(null);
|
const fitRef = useRef<FitAddon>(null);
|
||||||
|
|
@ -22,6 +63,86 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||||
if (saved) setColor(saved);
|
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 = () => {
|
const handleSave = () => {
|
||||||
localStorage.setItem("terminal-text-color", color);
|
localStorage.setItem("terminal-text-color", color);
|
||||||
onClose();
|
onClose();
|
||||||
|
|
@ -53,7 +174,7 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||||
if (xtermRef.current) terminal.current.open(xtermRef.current);
|
if (xtermRef.current) terminal.current.open(xtermRef.current);
|
||||||
|
|
||||||
terminal.current?.write(
|
terminal.current?.write(
|
||||||
"Change color \nChange color\nChange color\nChange color"
|
"Change color \nChange color\nChange color\nChange color",
|
||||||
);
|
);
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
}, [loaded, xtermRef]);
|
}, [loaded, xtermRef]);
|
||||||
|
|
@ -72,6 +193,7 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
|
fetchPrompts();
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
setLoaded(false);
|
setLoaded(false);
|
||||||
|
|
@ -84,78 +206,192 @@ export default function ModalConfig({ opened, onClose, onSave }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
size={"xl"}
|
size={"lg"}
|
||||||
style={{ position: "absolute", left: 0 }}
|
style={{ position: "absolute", left: 0 }}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Terminal Text Color"
|
title="Configuration"
|
||||||
>
|
>
|
||||||
<Grid>
|
<Tabs defaultValue="color">
|
||||||
<Grid.Col span={6}>
|
<Tabs.List justify="center">
|
||||||
<Group>
|
<Tabs.Tab value="color">Terminal Color</Tabs.Tab>
|
||||||
<ColorPicker
|
<Tabs.Tab value="prompt">Prompt AI</Tabs.Tab>
|
||||||
format="hex"
|
</Tabs.List>
|
||||||
value={color}
|
|
||||||
onChange={setColor}
|
<Tabs.Panel value="color">
|
||||||
fullWidth
|
<Grid style={{ height: "300px" }}>
|
||||||
withPicker={false}
|
<Grid.Col span={6}>
|
||||||
swatches={[
|
<Group>
|
||||||
"#ffffff",
|
<ColorPicker
|
||||||
"#41ee4a",
|
format="hex"
|
||||||
"#fa5252",
|
value={color}
|
||||||
"#e64980",
|
onChange={setColor}
|
||||||
"#be4bdb",
|
fullWidth
|
||||||
"#7950f2",
|
withPicker={false}
|
||||||
"#4c6ef5",
|
swatches={[
|
||||||
"#228be6",
|
"#ffffff",
|
||||||
"#15aabf",
|
"#41ee4a",
|
||||||
"#12b886",
|
"#fa5252",
|
||||||
"#40c057",
|
"#e64980",
|
||||||
"#82c91e",
|
"#be4bdb",
|
||||||
"#fab005",
|
"#7950f2",
|
||||||
"#fd7e14",
|
"#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}>
|
<Textarea
|
||||||
Save
|
label="Content"
|
||||||
</Button>
|
placeholder="Prompt content"
|
||||||
</Group>
|
value={formData.content}
|
||||||
</Grid.Col>
|
onChange={(e) =>
|
||||||
<Grid.Col span={6}>
|
setFormData({ ...formData, content: e.currentTarget.value })
|
||||||
<div
|
}
|
||||||
style={{
|
rows={20}
|
||||||
width: "100%",
|
required
|
||||||
height: "100%",
|
resize="vertical"
|
||||||
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>
|
<Group justify="flex-end">
|
||||||
</Grid>
|
<Button disabled={isDisabled} type="submit">
|
||||||
|
{editingId ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue