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:
nguyentrungthat 2026-05-06 10:32:45 +07:00
parent ef20557635
commit 1ae7550d77
8 changed files with 843 additions and 117 deletions

View File

@ -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,
})
}
}
}

View File

@ -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
}

View File

@ -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}
`,
}, },
], ],
} }

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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 đư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 )
---
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Ư ĐÃ show platform
KHÔNG đưc trả về INSUFFICIENT DATA thiếu show platform
---
Nếu thiếu lệnh bắt buộc khác:
RESULT: INSUFFICIENT DATA (missing <command>)
Ghi 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 :
* 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 lỗi hardware
PASS WITH WARNING nếu:
* Không lỗi hardware nhưng risk:
* Smart License
* Minor issue (AC low, warning logs, etc.)
LƯU Ý:
* PSU redundancy missing (chỉ 1 PSU hoặc PSU thứ 2 not present)
KHÔNG đưc đưa vào WARNING
Vẫn thể PASS nếu không 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 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 điện nhưng fail FAIL
* log "Power Supply Failure" FAIL
* Chỉ 1 PSU KHÔNG cảnh báo (no warning)
* AC low PASS WITH WARNING
==================================================
LICENSE RULE
============
Switch:
* 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 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" 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 đ ư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 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 , 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 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')
}
}
}

View File

@ -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')

View File

@ -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>
); );
} }