Upgrade Alias-Agent to 0.2.0 (#51)
Upgrade Alias-Agent to 0.2.0 --------- Co-authored-by: ZiTao-Li <zitao.l@alibaba-inc.com> Co-authored-by: xieyxclack <yuexiang.xyx@alibaba-inc.com> Co-authored-by: Zexi Li <tomleeze@qq.com> Co-authored-by: SSSuperDan <dlaura2218@gmail.com> Co-authored-by: lalaliat <78087788+lalaliat@users.noreply.github.com> Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com> Co-authored-by: Dengjiaji <dengjiaji.djj@alibaba-inc.com> Co-authored-by: 于南 <zengtianjing.ztj@alibaba-inc.com> Co-authored-by: JustinDing <166603159+sleepy-bird-world@users.noreply.github.com> Co-authored-by: y1y5 <269557841@qq.com> Co-authored-by: 柳佚 <yly287738@alibaba-inc.com> Co-authored-by: LiangguiWeng <347185100@qq.com> Co-authored-by: 潜星 <zhijian.mzj@alibaba-inc.com> Co-authored-by: StCarmen <1106135234@qq.com> Co-authored-by: LuYi <yilu_2000@outlook.com> Co-authored-by: 刺葳 <ciwei.cy@alibaba-inc.com>
45
alias/frontend/src/App.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import {
|
||||
ConfigProvider,
|
||||
carbonTheme,
|
||||
carbonDarkTheme,
|
||||
} from "@agentscope-ai/design";
|
||||
import { router } from "./routes";
|
||||
import { WorkspaceProvider } from "@/context/WorkspaceContext.tsx";
|
||||
import { ThemeProvider, useTheme } from "@/context/ThemeContext.tsx";
|
||||
import { MessageProvider } from "@/context/MessageContext";
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const { theme: currentTheme } = useTheme();
|
||||
const theme = currentTheme === "dark" ? carbonDarkTheme : carbonTheme;
|
||||
const prefix = "sps";
|
||||
return (
|
||||
<ConfigProvider
|
||||
{...theme}
|
||||
prefix={prefix}
|
||||
prefixCls={prefix}
|
||||
iconfont="//at.alicdn.com/t/a/font_4807885_ugexdeaoq7.js"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<WorkspaceProvider>
|
||||
<MessageProvider>
|
||||
<RouterProvider router={router} />
|
||||
</MessageProvider>
|
||||
</WorkspaceProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AppContent />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
501
alias/frontend/src/assets/file/incident_records.csv
Normal file
@@ -0,0 +1,501 @@
|
||||
category,state,closed_at,opened_at,closed_by,number,sys_updated_by,location,assigned_to,caller_id,sys_updated_on,short_description,assignment_group,priority
|
||||
Software,Closed,2023-01-03 11:04:00.000000000,2023-01-02 11:04:00,Charlie Whitherspoon,INC0000000000,admin,UK,Beth Anglin,David Loo,2023-01-03 11:04:00.000000000,Application error on office software,Software,2 - High
|
||||
Hardware,Closed,2023-01-11 01:17:39.128189467,2023-01-03 10:19:00,Beth Anglin,INC0000000001,employee,Australia,Luke Wilson,Don Goodliffe,2023-01-11 01:17:39.128189467,Printer error at Printer-id: Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-01-21 03:18:58.590910419,2023-01-04 06:37:00,Luke Wilson,INC0000000002,system,Australia,Fred Luddy,Don Goodliffe,2023-01-21 03:18:58.590910419,Printer745 is not working in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-01-05 17:54:36.886511927,2023-01-04 06:53:00,Luke Wilson,INC0000000003,employee,Australia,Fred Luddy,ITIL User,2023-01-05 17:54:36.886511927,Printer issue reported on Printer546,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-01-06 16:52:00.000000000,2023-01-05 16:52:00,Fred Luddy,INC0000000004,employee,Australia,Fred Luddy,David Loo,2023-01-06 16:52:00.000000000,Printer123 failure in Australia,Hardware,1 - Critical
|
||||
Network,Closed,2023-01-12 10:23:00.628645777,2023-01-06 01:22:00,Fred Luddy,INC0000000005,employee,India,Luke Wilson,ITIL User,2023-01-12 10:23:00.628645777,Unable to connect to the VPN from India,Network,1 - Critical
|
||||
Software,Resolved,2023-01-14 09:42:31.239968317,2023-01-06 06:14:00,Fred Luddy,INC0000000006,system,Canada,Luke Wilson,Bud Richman,2023-01-14 09:42:31.239968317,Software crash on startup does not allow to work properly,Software,1 - Critical
|
||||
Hardware,Closed,2023-01-13 03:44:31.383066130,2023-01-06 15:27:00,Luke Wilson,INC0000000007,admin,Australia,Charlie Whitherspoon,ITIL User,2023-01-13 03:44:31.383066130,Printer456 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-01-08 15:20:00.000000000,2023-01-07 15:20:00,Luke Wilson,INC0000000008,system,United States,Charlie Whitherspoon,David Loo,2023-01-08 15:20:00.000000000,Fault in server hardware S09-US,Hardware,2 - High
|
||||
Network,Resolved,2023-01-09 08:53:00.000000000,2023-01-08 08:53:00,Luke Wilson,INC0000000009,admin,UK,Beth Anglin,Bud Richman,2023-01-09 08:53:00.000000000,Can't connect to VPN,Network,2 - High
|
||||
Hardware,Closed,2023-01-18 10:47:18.859313409,2023-01-09 09:58:00,Charlie Whitherspoon,INC0000000010,system,Australia,Fred Luddy,Bud Richman,2023-01-18 10:47:18.859313409,Printer546 not working properly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-01-19 02:40:53.945578841,2023-01-09 16:07:00,Fred Luddy,INC0000000011,system,Australia,Fred Luddy,Bud Richman,2023-01-19 02:40:53.945578841,Printer354 is not working,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-01-13 22:01:58.804819178,2023-01-10 10:06:00,Luke Wilson,INC0000000012,admin,Australia,Luke Wilson,ITIL User,2023-01-13 22:01:58.804819178,Printer876 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-01-12 00:34:00.000000000,2023-01-11 00:34:00,Luke Wilson,INC0000000013,employee,UK,Charlie Whitherspoon,Don Goodliffe,2023-01-12 00:34:00.000000000,The monitor is not working properly,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-01-23 16:07:55.271807823,2023-01-11 09:26:00,Luke Wilson,INC0000000014,system,India,Howard Johnson,ITIL User,2023-01-23 16:07:55.271807823,Need help with Database query optimization,Database,3 - Moderate
|
||||
Hardware,Resolved,2023-01-20 14:37:18.361510788,2023-01-11 16:40:00,Howard Johnson,INC0000000015,system,Australia,Luke Wilson,Bud Richman,2023-01-20 14:37:18.361510788,Printer487 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-01-23 02:21:48.999352525,2023-01-11 18:29:00,Beth Anglin,INC0000000016,admin,United States,Fred Luddy,Don Goodliffe,2023-01-23 02:21:48.999352525,The system monitor of station 334 is not working,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-01-18 08:42:15.759148193,2023-01-13 00:44:00,Fred Luddy,INC0000000017,admin,Australia,Luke Wilson,David Loo,2023-01-18 08:42:15.759148193,Issue with Printer429 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-01-22 04:15:55.294347270,2023-01-15 14:01:00,Luke Wilson,INC0000000018,system,India,Luke Wilson,Don Goodliffe,2023-01-22 04:15:55.294347270,Device malfunction in CPU,Hardware,2 - High
|
||||
Inquiry / Help,Closed,2023-01-17 04:28:00.000000000,2023-01-16 04:28:00,Luke Wilson,INC0000000019,employee,United States,Fred Luddy,David Loo,2023-01-17 04:28:00.000000000,Need help with a software inquiry,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-01-26 02:18:22.920898595,2023-01-16 06:28:00,Beth Anglin,INC0000000020,employee,Australia,Howard Johnson,ITIL User,2023-01-26 02:18:22.920898595,Printer546 is not responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-01-25 07:44:05.814552632,2023-01-17 06:29:00,Fred Luddy,INC0000000021,admin,United States,Charlie Whitherspoon,ITIL User,2023-01-25 07:44:05.814552632,CPU fan failure on desktop unit,Hardware,2 - High
|
||||
Software,Resolved,2023-01-18 12:15:00.000000000,2023-01-17 12:15:00,Beth Anglin,INC0000000022,employee,Australia,Fred Luddy,Don Goodliffe,2023-01-18 12:15:00.000000000,Microsoft Office Application crash,Software,2 - High
|
||||
Hardware,Closed,2023-02-05 14:01:58.932181130,2023-01-17 13:34:00,Luke Wilson,INC0000000023,system,Australia,Fred Luddy,Bud Richman,2023-02-05 14:01:58.932181130,Printer546 has a paper jam,Hardware,2 - High
|
||||
Hardware,Resolved,2023-01-24 18:39:21.848415555,2023-01-17 17:47:00,Luke Wilson,INC0000000024,employee,Australia,Fred Luddy,David Loo,2023-01-24 18:39:21.848415555,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-01-23 14:49:23.734002375,2023-01-18 23:27:00,Fred Luddy,INC0000000025,admin,Australia,Fred Luddy,Bud Richman,2023-01-23 14:49:23.734002375,Printer546 has a paper jam,Hardware,2 - High
|
||||
Hardware,Closed,2023-01-24 10:21:12.713928509,2023-01-19 05:28:00,Howard Johnson,INC0000000026,admin,Australia,Beth Anglin,David Loo,2023-01-24 10:21:12.713928509,Printer546 is not functioning properly,Hardware,1 - Critical
|
||||
Software,Closed,2023-01-25 20:46:13.679914432,2023-01-19 17:29:00,Fred Luddy,INC0000000027,employee,Canada,Luke Wilson,Bud Richman,2023-01-25 20:46:13.679914432,Software update error on desktop,Software,2 - High
|
||||
Hardware,Closed,2023-02-04 02:43:53.446979933,2023-01-20 05:48:00,Fred Luddy,INC0000000028,system,Australia,Beth Anglin,ITIL User,2023-02-04 02:43:53.446979933,Printer546 is not responding,Hardware,2 - High
|
||||
Network,Resolved,2023-01-26 12:30:48.397896971,2023-01-21 12:13:00,Luke Wilson,INC0000000029,system,UK,Luke Wilson,David Loo,2023-01-26 12:30:48.397896971,Unable to connect to WiFi in UK office,Network,2 - High
|
||||
Hardware,Closed,2023-01-29 22:45:30.072558587,2023-01-22 03:34:00,Charlie Whitherspoon,INC0000000030,employee,Canada,Fred Luddy,Bud Richman,2023-01-29 22:45:30.072558587,PC overheating issue reported,Hardware,2 - High
|
||||
Hardware,Resolved,2023-02-05 00:09:42.176650334,2023-01-23 23:18:00,Howard Johnson,INC0000000031,admin,Australia,Howard Johnson,Bud Richman,2023-02-05 00:09:42.176650334,Printer546 is not printing documents in Australia,Hardware,2 - High
|
||||
Software,Closed,2023-01-28 01:28:09.743197803,2023-01-24 15:02:00,Luke Wilson,INC0000000032,system,Canada,Howard Johnson,ITIL User,2023-01-28 01:28:09.743197803,Software update failed on workstation,Software,2 - High
|
||||
Hardware,Resolved,2023-01-28 04:52:51.519667192,2023-01-25 02:38:00,Fred Luddy,INC0000000033,admin,Australia,Fred Luddy,Bud Richman,2023-01-28 04:52:51.519667192,Printer546 in Australia is offline,Hardware,1 - Critical
|
||||
Network,Resolved,2023-02-03 19:14:44.970166482,2023-01-25 20:52:00,Luke Wilson,INC0000000034,employee,United States,Beth Anglin,David Loo,2023-02-03 19:14:44.970166482,Can't connect to VPN,Network,2 - High
|
||||
Network,Resolved,2023-02-03 08:09:45.834493992,2023-01-27 02:22:00,Beth Anglin,INC0000000035,admin,UK,Charlie Whitherspoon,Don Goodliffe,2023-02-03 08:09:45.834493992,Network outage in the UK,Network,1 - Critical
|
||||
Hardware,Resolved,2023-02-11 09:07:59.479099672,2023-01-27 03:28:00,Howard Johnson,INC0000000036,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2023-02-11 09:07:59.479099672,Printer874 is not working,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-01-30 01:44:17.443653956,2023-01-27 07:48:00,Luke Wilson,INC0000000037,system,Australia,Beth Anglin,ITIL User,2023-01-30 01:44:17.443653956,Issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-02-03 19:29:33.426983106,2023-01-27 16:44:00,Howard Johnson,INC0000000038,system,Australia,Beth Anglin,Bud Richman,2023-02-03 19:29:33.426983106,Printer547 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-02-04 20:06:34.055632819,2023-01-30 17:07:00,Beth Anglin,INC0000000039,admin,India,Charlie Whitherspoon,ITIL User,2023-02-04 20:06:34.055632819,RAM issue in desktop PC,Hardware,2 - High
|
||||
Software,Closed,2023-02-07 09:37:36.868992308,2023-01-31 00:30:00,Luke Wilson,INC0000000040,employee,India,Howard Johnson,ITIL User,2023-02-07 09:37:36.868992308,Application crash on startup,Software,2 - High
|
||||
Hardware,Closed,2023-02-09 20:37:54.209291607,2023-02-01 19:46:00,Howard Johnson,INC0000000041,system,Australia,Luke Wilson,Bud Richman,2023-02-09 20:37:54.209291607,Printer789 is not printing in Australia location,Hardware,2 - High
|
||||
Network,Closed,2023-02-16 16:49:49.770102240,2023-02-02 08:10:00,Beth Anglin,INC0000000042,system,UK,Beth Anglin,ITIL User,2023-02-16 16:49:49.770102240,Unable to connect to WiFi,Network,2 - High
|
||||
Network,Closed,2023-02-13 00:16:27.398179564,2023-02-03 02:01:00,Charlie Whitherspoon,INC0000000043,system,United States,Charlie Whitherspoon,ITIL User,2023-02-13 00:16:27.398179564,Slow network connectivity issue across multiple devices,Network,2 - High
|
||||
Hardware,Closed,2023-02-14 04:41:35.236486606,2023-02-05 10:25:00,Luke Wilson,INC0000000044,employee,UK,Beth Anglin,Don Goodliffe,2023-02-14 04:41:35.236486606,Issue with Laptop Lp123,Hardware,2 - High
|
||||
Software,Closed,2023-02-18 11:04:04.387140839,2023-02-05 15:05:00,Luke Wilson,INC0000000045,admin,United States,Fred Luddy,Bud Richman,2023-02-18 11:04:04.387140839,Unable to update antivirus software,Software,2 - High
|
||||
Hardware,Resolved,2023-02-08 15:38:00.000000000,2023-02-07 15:38:00,Fred Luddy,INC0000000046,admin,Australia,Charlie Whitherspoon,David Loo,2023-02-08 15:38:00.000000000,Issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Database,Resolved,2023-02-13 08:10:20.378839709,2023-02-08 05:33:00,Howard Johnson,INC0000000047,system,India,Luke Wilson,Bud Richman,2023-02-13 08:10:20.378839709,Database server disruption detected,Database,1 - Critical
|
||||
Inquiry / Help,Closed,2023-02-17 00:34:25.927389861,2023-02-08 14:07:00,Beth Anglin,INC0000000048,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-02-17 00:34:25.927389861,Printer787 is causing problems,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-02-16 22:50:20.255626998,2023-02-11 04:10:00,Fred Luddy,INC0000000049,admin,Australia,Charlie Whitherspoon,Bud Richman,2023-02-16 22:50:20.255626998,Printer546 is not responding,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-02-13 00:56:00.000000000,2023-02-12 00:56:00,Howard Johnson,INC0000000050,system,UK,Fred Luddy,Bud Richman,2023-02-13 00:56:00.000000000,UK Server67 has stopped responding,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-02-13 09:00:00.000000000,2023-02-12 09:00:00,Howard Johnson,INC0000000051,admin,Australia,Fred Luddy,Bud Richman,2023-02-13 09:00:00.000000000,Printer546 is not responding,Hardware,1 - Critical
|
||||
Database,Resolved,2023-02-18 18:49:23.647006730,2023-02-12 10:41:00,Charlie Whitherspoon,INC0000000052,system,United States,Beth Anglin,David Loo,2023-02-18 18:49:23.647006730,SQL database connection issue,Database,3 - Moderate
|
||||
Inquiry / Help,Closed,2023-02-19 05:46:55.363664679,2023-02-14 16:34:00,Fred Luddy,INC0000000053,employee,Canada,Fred Luddy,ITIL User,2023-02-19 05:46:55.363664679,Need assistance with software installation,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-02-19 06:21:12.452625610,2023-02-15 01:47:00,Fred Luddy,INC0000000054,employee,United States,Fred Luddy,David Loo,2023-02-19 06:21:12.452625610,Laptop issue reported,Hardware,2 - High
|
||||
Hardware,Resolved,2023-02-19 11:12:41.587556967,2023-02-16 10:34:00,Luke Wilson,INC0000000055,employee,Australia,Howard Johnson,David Loo,2023-02-19 11:12:41.587556967,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-02-18 06:02:38.010888088,2023-02-16 17:07:00,Luke Wilson,INC0000000056,system,Australia,Beth Anglin,Don Goodliffe,2023-02-18 06:02:38.010888088,Printer546 is not working,Hardware,2 - High
|
||||
Network,Closed,2023-02-25 02:41:07.264125204,2023-02-16 18:29:00,Howard Johnson,INC0000000057,employee,India,Charlie Whitherspoon,Don Goodliffe,2023-02-25 02:41:07.264125204,Unable to connect to the VPN,Network,2 - High
|
||||
Hardware,Resolved,2023-02-26 07:36:08.762766728,2023-02-16 19:04:00,Fred Luddy,INC0000000058,system,Australia,Charlie Whitherspoon,David Loo,2023-02-26 07:36:08.762766728,Printer546 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-02-19 01:46:00.000000000,2023-02-18 01:46:00,Luke Wilson,INC0000000059,employee,Australia,Fred Luddy,ITIL User,2023-02-19 01:46:00.000000000,Printer543 is not printing in Australia,Hardware,1 - Critical
|
||||
Database,Closed,2023-02-24 12:26:03.066179768,2023-02-18 16:04:00,Beth Anglin,INC0000000060,admin,Australia,Howard Johnson,David Loo,2023-02-24 12:26:03.066179768,Issue with database response time,Database,2 - High
|
||||
Inquiry / Help,Resolved,2023-03-05 11:31:06.569025031,2023-02-22 12:21:00,Beth Anglin,INC0000000061,admin,United States,Luke Wilson,ITIL User,2023-03-05 11:31:06.569025031,Request assistance for application updating issues,Software,3 - Moderate
|
||||
Hardware,Closed,2023-02-23 22:00:23.401697490,2023-02-22 13:49:00,Charlie Whitherspoon,INC0000000062,employee,Australia,Howard Johnson,ITIL User,2023-02-23 22:00:23.401697490,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Network,Resolved,2023-02-26 16:29:16.807931635,2023-02-22 19:23:00,Luke Wilson,INC0000000063,employee,India,Beth Anglin,Bud Richman,2023-02-26 16:29:16.807931635,Network connectivity issue in India,Network,2 - High
|
||||
Hardware,Resolved,2023-02-25 14:32:02.076278529,2023-02-23 02:37:00,Beth Anglin,INC0000000064,system,Australia,Charlie Whitherspoon,David Loo,2023-02-25 14:32:02.076278529,Printer error in Printer368,Hardware,2 - High
|
||||
Hardware,Closed,2023-03-01 04:41:50.658060449,2023-02-23 05:40:00,Beth Anglin,INC0000000065,system,Canada,Beth Anglin,David Loo,2023-03-01 04:41:50.658060449,"Issue with desktop PC, unexpected shutdown",Hardware,2 - High
|
||||
Database,Closed,2023-03-02 18:17:24.556975522,2023-02-25 17:52:00,Fred Luddy,INC0000000066,system,India,Luke Wilson,David Loo,2023-03-02 18:17:24.556975522,Unable to connect to server database,Database,2 - High
|
||||
Hardware,Resolved,2023-03-03 22:24:21.666904166,2023-02-25 19:47:00,Charlie Whitherspoon,INC0000000067,employee,Australia,Howard Johnson,Don Goodliffe,2023-03-03 22:24:21.666904166,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-03-11 20:13:52.693225203,2023-02-25 21:22:00,Fred Luddy,INC0000000068,employee,Australia,Luke Wilson,Don Goodliffe,2023-03-11 20:13:52.693225203,Printer problem with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-02-28 16:37:00.000000000,2023-02-27 16:37:00,Charlie Whitherspoon,INC0000000069,employee,Australia,Beth Anglin,Don Goodliffe,2023-02-28 16:37:00.000000000,Printer546 is not functioning,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-05 04:00:53.829740603,2023-02-27 21:40:00,Fred Luddy,INC0000000070,admin,Australia,Luke Wilson,Bud Richman,2023-03-05 04:00:53.829740603,Printer453 is not responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-03-17 12:55:54.993590364,2023-02-28 17:42:00,Charlie Whitherspoon,INC0000000071,admin,Australia,Fred Luddy,ITIL User,2023-03-17 12:55:54.993590364,Printer567 is not functioning properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-03-13 16:49:10.116400647,2023-03-02 01:29:00,Beth Anglin,INC0000000072,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-03-13 16:49:10.116400647,Issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Inquiry / Help,Resolved,2023-03-08 05:56:31.551604697,2023-03-02 10:56:00,Beth Anglin,INC0000000073,admin,Canada,Fred Luddy,Don Goodliffe,2023-03-08 05:56:31.551604697,Inquiry about the service in Canada,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-03-11 13:42:59.511508874,2023-03-04 03:00:00,Fred Luddy,INC0000000074,system,Australia,Charlie Whitherspoon,Bud Richman,2023-03-11 13:42:59.511508874,Printer546 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-03-19 18:35:49.186969810,2023-03-07 06:18:00,Howard Johnson,INC0000000075,admin,Australia,Charlie Whitherspoon,Bud Richman,2023-03-19 18:35:49.186969810,Printer546 is not printing in Australia location. Please assist.,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-17 15:33:56.380678280,2023-03-08 16:23:00,Howard Johnson,INC0000000076,system,India,Beth Anglin,David Loo,2023-03-17 15:33:56.380678280,Server rack 34B malfunctioning in Chennai data center,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-14 06:30:47.245274154,2023-03-09 02:01:00,Luke Wilson,INC0000000077,employee,Australia,Fred Luddy,ITIL User,2023-03-14 06:30:47.245274154,Printer546 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-03-10 09:09:00.000000000,2023-03-09 09:09:00,Luke Wilson,INC0000000078,employee,Australia,Fred Luddy,ITIL User,2023-03-10 09:09:00.000000000,Printer546 in Australia is causing an issue,Hardware,1 - Critical
|
||||
Network,Resolved,2023-03-11 09:09:00.000000000,2023-03-10 09:09:00,Charlie Whitherspoon,INC0000000079,employee,UK,Charlie Whitherspoon,Bud Richman,2023-03-11 09:09:00.000000000,Network connection lost in UK,Network,1 - Critical
|
||||
Network,Closed,2023-03-13 09:38:15.903837660,2023-03-10 14:49:00,Fred Luddy,INC0000000080,employee,India,Fred Luddy,ITIL User,2023-03-13 09:38:15.903837660,Unable to establish a connection to the local server,Network,2 - High
|
||||
Database,Closed,2023-03-17 12:37:48.453885624,2023-03-11 06:39:00,Luke Wilson,INC0000000081,system,United States,Beth Anglin,Bud Richman,2023-03-17 12:37:48.453885624,Unable to connect to Database567,Database,2 - High
|
||||
Hardware,Closed,2023-03-16 11:23:45.848878332,2023-03-13 02:53:00,Beth Anglin,INC0000000082,admin,United States,Howard Johnson,David Loo,2023-03-16 11:23:45.848878332,Hard drive failure in laptop HL349,Hardware,2 - High
|
||||
Database,Closed,2023-03-26 23:30:22.193990107,2023-03-13 07:44:00,Fred Luddy,INC0000000083,system,Canada,Charlie Whitherspoon,Bud Richman,2023-03-26 23:30:22.193990107,Unable to connect to SQL Database 219,Database,2 - High
|
||||
Hardware,Closed,2023-03-14 22:49:22.720352152,2023-03-13 10:05:00,Fred Luddy,INC0000000084,system,Australia,Luke Wilson,David Loo,2023-03-14 22:49:22.720352152,Issue with Printer210,Hardware,2 - High
|
||||
Hardware,Closed,2023-03-18 09:32:55.751173465,2023-03-13 11:36:00,Charlie Whitherspoon,INC0000000085,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-03-18 09:32:55.751173465,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-23 14:56:55.026865443,2023-03-14 00:04:00,Charlie Whitherspoon,INC0000000086,admin,Australia,Luke Wilson,David Loo,2023-03-23 14:56:55.026865443,Issue with Printer546 in Australia office,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-26 11:10:50.949076210,2023-03-17 00:19:00,Charlie Whitherspoon,INC0000000087,admin,Australia,Howard Johnson,David Loo,2023-03-26 11:10:50.949076210,Printer546 is malfunctioning,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-03-18 07:40:00.000000000,2023-03-17 07:40:00,Charlie Whitherspoon,INC0000000088,employee,Australia,Fred Luddy,David Loo,2023-03-18 07:40:00.000000000,Printer546 has a paper jam,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-22 15:24:16.646216503,2023-03-18 01:04:00,Charlie Whitherspoon,INC0000000089,employee,Australia,Fred Luddy,David Loo,2023-03-22 15:24:16.646216503,Printer546 is malfunctioning in Australia,Hardware,1 - Critical
|
||||
Software,Resolved,2023-03-26 17:14:15.209900642,2023-03-19 09:40:00,Charlie Whitherspoon,INC0000000090,system,United States,Fred Luddy,Don Goodliffe,2023-03-26 17:14:15.209900642,Software update failure on work PC,Software,3 - Moderate
|
||||
Hardware,Resolved,2023-03-21 01:02:44.587646914,2023-03-19 12:57:00,Fred Luddy,INC0000000091,admin,Australia,Beth Anglin,David Loo,2023-03-21 01:02:44.587646914,Printer546 is not working,Hardware,1 - Critical
|
||||
Network,Resolved,2023-03-31 07:59:26.909209525,2023-03-19 14:53:00,Howard Johnson,INC0000000092,employee,Australia,Fred Luddy,Bud Richman,2023-03-31 07:59:26.909209525,Network downtime issue in Australia,Network,1 - Critical
|
||||
Inquiry / Help,Closed,2023-04-04 19:04:22.181856865,2023-03-22 13:51:00,Fred Luddy,INC0000000093,admin,United States,Charlie Whitherspoon,ITIL User,2023-04-04 19:04:22.181856865,Need help with software installation,Service Desk,2 - High
|
||||
Hardware,Resolved,2023-03-29 18:40:47.611595618,2023-03-22 14:07:00,Howard Johnson,INC0000000094,system,Australia,Howard Johnson,David Loo,2023-03-29 18:40:47.611595618,Printer546 is having a paper jam issue,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-04-13 00:44:40.599581505,2023-03-22 20:33:00,Luke Wilson,INC0000000095,admin,India,Beth Anglin,ITIL User,2023-04-13 00:44:40.599581505,Server error on HDD2385,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-31 08:03:25.091057805,2023-03-23 07:51:00,Charlie Whitherspoon,INC0000000096,admin,Australia,Luke Wilson,David Loo,2023-03-31 08:03:25.091057805,Printer546 is not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-30 05:18:20.748165094,2023-03-23 16:57:00,Luke Wilson,INC0000000097,employee,UK,Charlie Whitherspoon,David Loo,2023-03-30 05:18:20.748165094,Issue with workstation PC238,Hardware,3 - Moderate
|
||||
Software,Closed,2023-04-08 08:46:45.358954276,2023-03-23 22:07:00,Beth Anglin,INC0000000098,system,India,Howard Johnson,Bud Richman,2023-04-08 08:46:45.358954276,Bug detected in accounting software,Software,2 - High
|
||||
Hardware,Closed,2023-03-29 09:27:22.091188363,2023-03-26 03:15:00,Luke Wilson,INC0000000099,employee,Australia,Charlie Whitherspoon,David Loo,2023-03-29 09:27:22.091188363,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Resolved,2023-04-08 15:04:33.153395278,2023-03-26 20:37:00,Fred Luddy,INC0000000100,system,Australia,Beth Anglin,ITIL User,2023-04-08 15:04:33.153395278,Printer546 in Australia is not working,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-04-08 07:50:02.291054808,2023-03-27 23:22:00,Fred Luddy,INC0000000101,system,Canada,Charlie Whitherspoon,Bud Richman,2023-04-08 07:50:02.291054808,Laptop973 malfunction in Canada location,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-04-04 09:15:27.409492338,2023-03-29 01:49:00,Fred Luddy,INC0000000102,employee,Australia,Fred Luddy,Don Goodliffe,2023-04-04 09:15:27.409492338,Printer546 is down,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-04-05 05:58:43.770711476,2023-03-29 05:22:00,Luke Wilson,INC0000000103,admin,India,Fred Luddy,Don Goodliffe,2023-04-05 05:58:43.770711476,Server076 overheating issue,Hardware,2 - High
|
||||
Hardware,Resolved,2023-03-31 22:32:42.024027147,2023-03-30 00:58:00,Charlie Whitherspoon,INC0000000104,system,Australia,Charlie Whitherspoon,David Loo,2023-03-31 22:32:42.024027147,Printer546 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-04-16 00:36:30.146701781,2023-04-01 09:01:00,Luke Wilson,INC0000000105,employee,Australia,Beth Anglin,David Loo,2023-04-16 00:36:30.146701781,Printer issue with Printer546 in Australia,Hardware,3 - Moderate
|
||||
Network,Resolved,2023-04-08 23:37:33.628402771,2023-04-02 15:37:00,Howard Johnson,INC0000000106,system,Australia,Fred Luddy,Bud Richman,2023-04-08 23:37:33.628402771,Internet connectivity issue with Australian network,Network,2 - High
|
||||
Hardware,Closed,2023-04-13 19:24:58.346273013,2023-04-03 23:10:00,Howard Johnson,INC0000000107,employee,United States,Fred Luddy,ITIL User,2023-04-13 19:24:58.346273013,US Server Unresponsive,Hardware,2 - High
|
||||
Hardware,Resolved,2023-04-09 09:12:14.485573467,2023-04-04 04:43:00,Charlie Whitherspoon,INC0000000108,admin,Australia,Luke Wilson,Don Goodliffe,2023-04-09 09:12:14.485573467,Issue with Printer276 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-04-17 20:17:35.569877984,2023-04-05 00:45:00,Fred Luddy,INC0000000109,admin,Australia,Fred Luddy,Bud Richman,2023-04-17 20:17:35.569877984,Printer546 is not working in Australia location,Hardware,2 - High
|
||||
Network,Resolved,2023-04-13 15:50:15.123078287,2023-04-08 12:59:00,Charlie Whitherspoon,INC0000000110,employee,Australia,Howard Johnson,Don Goodliffe,2023-04-13 15:50:15.123078287,Network outage reported,Network,1 - Critical
|
||||
Hardware,Resolved,2023-04-13 02:17:59.600859991,2023-04-08 22:13:00,Charlie Whitherspoon,INC0000000111,employee,Australia,Luke Wilson,Bud Richman,2023-04-13 02:17:59.600859991,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Database,Closed,2023-04-15 04:23:54.073331834,2023-04-09 10:28:00,Beth Anglin,INC0000000112,employee,India,Fred Luddy,David Loo,2023-04-15 04:23:54.073331834,Unable to connect to SQL Database,Database,3 - Moderate
|
||||
Hardware,Resolved,2023-04-17 11:40:20.915287122,2023-04-09 15:33:00,Beth Anglin,INC0000000113,admin,Australia,Fred Luddy,Don Goodliffe,2023-04-17 11:40:20.915287122,Printer error on Printer546,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-04-24 09:36:05.775625483,2023-04-10 12:12:00,Beth Anglin,INC0000000114,employee,Australia,Fred Luddy,Bud Richman,2023-04-24 09:36:05.775625483,Printer546 is not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-04-13 12:12:50.918205479,2023-04-11 08:43:00,Charlie Whitherspoon,INC0000000115,employee,Australia,Beth Anglin,Don Goodliffe,2023-04-13 12:12:50.918205479,Printer546 is not responding,Hardware,2 - High
|
||||
Hardware,Resolved,2023-04-22 08:16:59.883429480,2023-04-11 11:50:00,Beth Anglin,INC0000000116,admin,UK,Luke Wilson,ITIL User,2023-04-22 08:16:59.883429480,UK Printer546 malfunctioning,Hardware,2 - High
|
||||
Network,Resolved,2023-04-13 09:52:00.000000000,2023-04-12 09:52:00,Charlie Whitherspoon,INC0000000117,employee,Canada,Beth Anglin,Bud Richman,2023-04-13 09:52:00.000000000,Network outage at Canada site,Network,1 - Critical
|
||||
Network,Resolved,2023-04-13 12:13:00.000000000,2023-04-12 12:13:00,Charlie Whitherspoon,INC0000000118,employee,UK,Fred Luddy,ITIL User,2023-04-13 12:13:00.000000000,Network failure in region UK,Network,1 - Critical
|
||||
Hardware,Closed,2023-04-14 10:00:00.000000000,2023-04-13 10:00:00,Howard Johnson,INC0000000119,employee,Australia,Luke Wilson,Don Goodliffe,2023-04-14 10:00:00.000000000,Printer639 is not working properly,Hardware,1 - Critical
|
||||
Database,Closed,2023-04-25 00:40:17.584686390,2023-04-14 23:54:00,Luke Wilson,INC0000000120,system,India,Charlie Whitherspoon,Bud Richman,2023-04-25 00:40:17.584686390,Database query optimization issue at India location,Database,3 - Moderate
|
||||
Hardware,Closed,2023-04-25 04:21:38.122505056,2023-04-15 11:57:00,Howard Johnson,INC0000000121,employee,Australia,Fred Luddy,ITIL User,2023-04-25 04:21:38.122505056,Printer789 is not responding,Hardware,2 - High
|
||||
Network,Resolved,2023-04-19 16:48:00.000000000,2023-04-18 16:48:00,Charlie Whitherspoon,INC0000000122,system,UK,Fred Luddy,Don Goodliffe,2023-04-19 16:48:00.000000000,Network connection issue in UK,Network,2 - High
|
||||
Hardware,Closed,2023-05-03 23:18:07.556497402,2023-04-18 19:22:00,Howard Johnson,INC0000000123,employee,Australia,Beth Anglin,Don Goodliffe,2023-05-03 23:18:07.556497402,Printer788 is not working properly,Hardware,1 - Critical
|
||||
Network,Closed,2023-04-24 02:25:13.899200115,2023-04-18 22:20:00,Fred Luddy,INC0000000124,system,India,Howard Johnson,Bud Richman,2023-04-24 02:25:13.899200115,Network outage in India location,Network,1 - Critical
|
||||
Database,Resolved,2023-04-26 00:28:50.913657583,2023-04-19 07:07:00,Fred Luddy,INC0000000125,employee,India,Luke Wilson,ITIL User,2023-04-26 00:28:50.913657583,Database Server Outage,Database,2 - High
|
||||
Hardware,Resolved,2023-04-30 20:00:34.805177671,2023-04-19 14:41:00,Fred Luddy,INC0000000126,employee,Australia,Charlie Whitherspoon,ITIL User,2023-04-30 20:00:34.805177671,Printer556 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-04-26 13:43:55.256743773,2023-04-20 22:34:00,Beth Anglin,INC0000000127,admin,Australia,Howard Johnson,Don Goodliffe,2023-04-26 13:43:55.256743773,Printer454 failed in office,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-05-05 00:21:12.204265630,2023-04-21 03:46:00,Beth Anglin,INC0000000128,system,United States,Luke Wilson,Don Goodliffe,2023-05-05 00:21:12.204265630,Desktop PC not turning on,Hardware,2 - High
|
||||
Software,Closed,2023-05-06 00:19:06.845921162,2023-04-21 08:13:00,Charlie Whitherspoon,INC0000000129,employee,Australia,Luke Wilson,David Loo,2023-05-06 00:19:06.845921162,Software license renewal required,Software,4 - Low
|
||||
Hardware,Closed,2023-04-29 07:22:13.691497066,2023-04-21 17:22:00,Luke Wilson,INC0000000130,system,UK,Charlie Whitherspoon,David Loo,2023-04-29 07:22:13.691497066,Issue with UK server hardware,Hardware,2 - High
|
||||
Hardware,Closed,2023-04-26 07:15:31.647958414,2023-04-22 10:55:00,Howard Johnson,INC0000000131,system,Australia,Luke Wilson,David Loo,2023-04-26 07:15:31.647958414,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-10 03:20:31.007719853,2023-04-23 21:20:00,Howard Johnson,INC0000000132,system,Australia,Fred Luddy,David Loo,2023-05-10 03:20:31.007719853,Printer827 is not working properly in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-05-10 23:55:09.253391223,2023-04-27 03:25:00,Howard Johnson,INC0000000133,system,UK,Charlie Whitherspoon,Don Goodliffe,2023-05-10 23:55:09.253391223,Faulty keyboard on workstation,Hardware,2 - High
|
||||
Network,Closed,2023-05-05 01:59:48.858836394,2023-04-27 10:45:00,Luke Wilson,INC0000000134,admin,UK,Charlie Whitherspoon,ITIL User,2023-05-05 01:59:48.858836394,Network outage in UK site,Network,1 - Critical
|
||||
Hardware,Resolved,2023-05-06 08:53:56.696181202,2023-04-28 01:24:00,Luke Wilson,INC0000000135,system,Australia,Howard Johnson,David Loo,2023-05-06 08:53:56.696181202,Printer547 isn't responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-14 08:03:47.192602517,2023-04-30 00:53:00,Charlie Whitherspoon,INC0000000136,admin,India,Howard Johnson,Don Goodliffe,2023-05-14 08:03:47.192602517,Issue with the server hardware,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-05 19:25:33.306689559,2023-05-02 14:34:00,Beth Anglin,INC0000000137,employee,Australia,Luke Wilson,David Loo,2023-05-05 19:25:33.306689559,Printer546 is broken in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-05-08 00:19:49.950083121,2023-05-02 15:01:00,Luke Wilson,INC0000000138,admin,Australia,Howard Johnson,Don Goodliffe,2023-05-08 00:19:49.950083121,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-15 10:38:15.815796741,2023-05-03 04:24:00,Charlie Whitherspoon,INC0000000139,admin,Australia,Beth Anglin,Don Goodliffe,2023-05-15 10:38:15.815796741,Printer472 in Australia is not responding,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-05-04 21:57:00.000000000,2023-05-03 21:57:00,Beth Anglin,INC0000000140,admin,UK,Howard Johnson,David Loo,2023-05-04 21:57:00.000000000,UK Desktop PC with ID: Desktop123 has stopped working,Hardware,2 - High
|
||||
Software,Closed,2023-05-14 06:26:59.910015516,2023-05-04 13:17:00,Luke Wilson,INC0000000141,system,India,Charlie Whitherspoon,Bud Richman,2023-05-14 06:26:59.910015516,Unable to install new software,Software,2 - High
|
||||
Hardware,Resolved,2023-05-12 02:21:34.844909705,2023-05-05 07:09:00,Beth Anglin,INC0000000142,employee,Australia,Beth Anglin,David Loo,2023-05-12 02:21:34.844909705,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-19 09:53:34.982040620,2023-05-05 20:05:00,Charlie Whitherspoon,INC0000000143,admin,India,Beth Anglin,David Loo,2023-05-19 09:53:34.982040620,Server failure - server123,Hardware,1 - Critical
|
||||
Database,Closed,2023-05-10 22:35:58.881919516,2023-05-05 20:26:00,Fred Luddy,INC0000000144,admin,United States,Charlie Whitherspoon,David Loo,2023-05-10 22:35:58.881919516,Unable to access the database,Database,2 - High
|
||||
Inquiry / Help,Closed,2023-05-06 22:24:00.000000000,2023-05-05 22:24:00,Charlie Whitherspoon,INC0000000145,system,Canada,Charlie Whitherspoon,Don Goodliffe,2023-05-06 22:24:00.000000000,Inquiry about help on local systems,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-05-13 14:49:23.379483573,2023-05-07 09:01:00,Fred Luddy,INC0000000146,employee,Australia,Luke Wilson,ITIL User,2023-05-13 14:49:23.379483573,Printer546 issue in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-13 13:14:46.388208661,2023-05-07 11:12:00,Howard Johnson,INC0000000147,system,Australia,Luke Wilson,Bud Richman,2023-05-13 13:14:46.388208661,Issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-14 12:40:40.813937606,2023-05-07 12:05:00,Beth Anglin,INC0000000148,system,Canada,Howard Johnson,ITIL User,2023-05-14 12:40:40.813937606,Issue with Dell521 Desktop PC malfunctioning,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-24 17:45:04.375124775,2023-05-12 06:51:00,Charlie Whitherspoon,INC0000000149,system,Australia,Luke Wilson,ITIL User,2023-05-24 17:45:04.375124775,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-05-19 04:22:50.443252112,2023-05-12 11:38:00,Beth Anglin,INC0000000150,admin,Australia,Beth Anglin,David Loo,2023-05-19 04:22:50.443252112,Printer546 is not printing,Hardware,1 - Critical
|
||||
Software,Resolved,2023-05-19 01:48:03.565852983,2023-05-13 02:54:00,Luke Wilson,INC0000000151,employee,India,Howard Johnson,Don Goodliffe,2023-05-19 01:48:03.565852983,Software update failed on the server,Software,3 - Moderate
|
||||
Hardware,Resolved,2023-05-24 09:29:18.509870284,2023-05-13 10:11:00,Fred Luddy,INC0000000152,admin,Australia,Howard Johnson,Don Goodliffe,2023-05-24 09:29:18.509870284,Issue with Printer546 in Australia,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-05-19 07:25:36.115649042,2023-05-14 11:45:00,Fred Luddy,INC0000000153,admin,Australia,Luke Wilson,Don Goodliffe,2023-05-19 07:25:36.115649042,Printer546 has stopped working in Australia,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-05-18 20:30:00.000000000,2023-05-17 20:30:00,Fred Luddy,INC0000000154,employee,Australia,Howard Johnson,David Loo,2023-05-18 20:30:00.000000000,Printer876 is not printing,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-27 00:02:45.297437201,2023-05-18 07:29:00,Luke Wilson,INC0000000155,employee,Australia,Fred Luddy,Bud Richman,2023-05-27 00:02:45.297437201,Printer546 malfunction in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-05-29 16:00:46.480174225,2023-05-18 17:29:00,Fred Luddy,INC0000000156,system,India,Fred Luddy,Don Goodliffe,2023-05-29 16:00:46.480174225,Issue with Dell Inspiron 15,Hardware,2 - High
|
||||
Database,Closed,2023-06-05 23:47:52.328722644,2023-05-18 22:32:00,Howard Johnson,INC0000000157,employee,India,Fred Luddy,ITIL User,2023-06-05 23:47:52.328722644,SQL error on Production Server,Database,2 - High
|
||||
Hardware,Resolved,2023-06-03 13:13:53.896117331,2023-05-19 04:05:00,Charlie Whitherspoon,INC0000000158,admin,Australia,Howard Johnson,Bud Richman,2023-06-03 13:13:53.896117331,Printer546 encountered an issue,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-24 07:52:48.690310999,2023-05-20 06:12:00,Beth Anglin,INC0000000159,system,Australia,Charlie Whitherspoon,David Loo,2023-05-24 07:52:48.690310999,Printer issue with Printer546 in Australia,Hardware,2 - High
|
||||
Network,Closed,2023-05-31 03:23:19.484544434,2023-05-21 05:49:00,Howard Johnson,INC0000000160,employee,India,Charlie Whitherspoon,ITIL User,2023-05-31 03:23:19.484544434,Network outage in India region,Network,2 - High
|
||||
Hardware,Closed,2023-05-22 23:24:00.000000000,2023-05-21 23:24:00,Howard Johnson,INC0000000161,system,India,Beth Anglin,ITIL User,2023-05-22 23:24:00.000000000,Server failure - static issue,Hardware,2 - High
|
||||
Hardware,Resolved,2023-05-31 14:07:51.086048950,2023-05-22 01:22:00,Beth Anglin,INC0000000162,system,Australia,Fred Luddy,Bud Richman,2023-05-31 14:07:51.086048950,Printer546 malfunction in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-07 16:18:08.061177566,2023-05-24 02:06:00,Fred Luddy,INC0000000163,admin,United States,Howard Johnson,Bud Richman,2023-06-07 16:18:08.061177566,Issue with processing unit in desktop PC,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-05-27 02:04:45.048501853,2023-05-24 03:55:00,Luke Wilson,INC0000000164,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2023-05-27 02:04:45.048501853,Issue with Printer457 in Australia office,Hardware,2 - High
|
||||
Hardware,Resolved,2023-05-30 02:59:49.369927031,2023-05-26 08:08:00,Fred Luddy,INC0000000165,employee,Australia,Luke Wilson,David Loo,2023-05-30 02:59:49.369927031,Printer241 issue in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-30 11:21:04.259433606,2023-05-26 13:20:00,Fred Luddy,INC0000000166,system,Australia,Charlie Whitherspoon,David Loo,2023-05-30 11:21:04.259433606,Printer546 failure in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-05-28 08:38:00.000000000,2023-05-27 08:38:00,Charlie Whitherspoon,INC0000000167,admin,India,Luke Wilson,David Loo,2023-05-28 08:38:00.000000000,Router problem with RTR546 in India,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-01 13:53:33.745863862,2023-05-27 21:00:00,Fred Luddy,INC0000000168,system,Australia,Charlie Whitherspoon,ITIL User,2023-06-01 13:53:33.745863862,Printer error on Printer643,Hardware,2 - High
|
||||
Network,Resolved,2023-05-29 13:25:00.000000000,2023-05-28 13:25:00,Luke Wilson,INC0000000169,system,India,Howard Johnson,Don Goodliffe,2023-05-29 13:25:00.000000000,Network outage in Mumbai region,Network,1 - Critical
|
||||
Software,Resolved,2023-06-07 06:19:03.182476115,2023-05-29 04:20:00,Beth Anglin,INC0000000170,admin,Canada,Beth Anglin,ITIL User,2023-06-07 06:19:03.182476115,Unable to install new software update,Software,2 - High
|
||||
Software,Resolved,2023-06-07 15:04:30.359478777,2023-05-30 09:43:00,Luke Wilson,INC0000000171,system,Australia,Fred Luddy,David Loo,2023-06-07 15:04:30.359478777,Software update required,Software,2 - High
|
||||
Database,Resolved,2023-06-04 09:17:41.184530679,2023-05-30 09:56:00,Luke Wilson,INC0000000172,admin,Canada,Howard Johnson,Don Goodliffe,2023-06-04 09:17:41.184530679,Issue with performance of database DB202,Database,2 - High
|
||||
Hardware,Closed,2023-06-02 00:47:00.000000000,2023-06-01 00:47:00,Charlie Whitherspoon,INC0000000173,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-06-02 00:47:00.000000000,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-13 22:57:17.038108358,2023-06-01 13:13:00,Luke Wilson,INC0000000174,system,Canada,Charlie Whitherspoon,ITIL User,2023-06-13 22:57:17.038108358,Failed Hard Disk on Workstation122,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-11 09:48:35.368342810,2023-06-02 12:28:00,Luke Wilson,INC0000000175,admin,Australia,Beth Anglin,Bud Richman,2023-06-11 09:48:35.368342810,Printer fail at printer547,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-11 20:25:35.094482408,2023-06-02 22:47:00,Howard Johnson,INC0000000176,admin,UK,Charlie Whitherspoon,ITIL User,2023-06-11 20:25:35.094482408,"Laptop malfunctioning, requires immediate attention",Hardware,2 - High
|
||||
Hardware,Closed,2023-06-17 06:06:14.854314828,2023-06-03 10:45:00,Luke Wilson,INC0000000177,system,Canada,Beth Anglin,Bud Richman,2023-06-17 06:06:14.854314828,Hardware malfunction for laptop - Canada location,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-06 12:59:19.008350345,2023-06-03 12:39:00,Luke Wilson,INC0000000178,employee,Canada,Fred Luddy,ITIL User,2023-06-06 12:59:19.008350345,Issue with the server hardware,Hardware,2 - High
|
||||
Software,Closed,2023-06-05 15:09:00.000000000,2023-06-04 15:09:00,Howard Johnson,INC0000000179,system,Canada,Howard Johnson,Don Goodliffe,2023-06-05 15:09:00.000000000,An application is consistently crashing,Software,2 - High
|
||||
Hardware,Resolved,2023-06-09 23:10:07.618999909,2023-06-04 21:23:00,Howard Johnson,INC0000000180,employee,Australia,Charlie Whitherspoon,David Loo,2023-06-09 23:10:07.618999909,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-06 08:20:00.000000000,2023-06-05 08:20:00,Luke Wilson,INC0000000181,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-06-06 08:20:00.000000000,Printer546 is not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-19 06:59:02.120925069,2023-06-06 09:03:00,Howard Johnson,INC0000000182,employee,India,Fred Luddy,David Loo,2023-06-19 06:59:02.120925069,Server malfunction,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-07 21:23:00.000000000,2023-06-06 21:23:00,Luke Wilson,INC0000000183,employee,Australia,Beth Anglin,Don Goodliffe,2023-06-07 21:23:00.000000000,Printer567 is not working in Brisbane office,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-30 13:34:11.629362629,2023-06-07 02:47:00,Howard Johnson,INC0000000184,system,Australia,Fred Luddy,David Loo,2023-06-30 13:34:11.629362629,Printer546 is not working in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-14 11:45:24.784548040,2023-06-07 15:24:00,Charlie Whitherspoon,INC0000000185,employee,United States,Howard Johnson,David Loo,2023-06-14 11:45:24.784548040,Monitor display not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-08 17:00:00.000000000,2023-06-07 17:00:00,Beth Anglin,INC0000000186,system,Australia,Charlie Whitherspoon,David Loo,2023-06-08 17:00:00.000000000,Printer546 - Printer not working,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-06-20 19:52:27.802682144,2023-06-08 18:44:00,Charlie Whitherspoon,INC0000000187,employee,Australia,Beth Anglin,David Loo,2023-06-20 19:52:27.802682144,Printer123 is not working properly in Australia location,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-06-13 20:13:50.263024628,2023-06-08 22:05:00,Howard Johnson,INC0000000188,employee,Australia,Luke Wilson,Don Goodliffe,2023-06-13 20:13:50.263024628,Printer546 issue in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-24 09:17:21.801903137,2023-06-09 08:53:00,Luke Wilson,INC0000000189,system,Australia,Luke Wilson,ITIL User,2023-06-24 09:17:21.801903137,Printer error in Printer546,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-15 22:13:18.416949743,2023-06-10 17:52:00,Beth Anglin,INC0000000190,admin,Australia,Luke Wilson,ITIL User,2023-06-15 22:13:18.416949743,Printer error: Paper jam on Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-12 02:12:20.844915289,2023-06-10 20:44:00,Beth Anglin,INC0000000191,system,Australia,Luke Wilson,Bud Richman,2023-06-12 02:12:20.844915289,Printer546 is not printing in the Melbourne office,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-06-18 08:09:47.068608027,2023-06-11 07:00:00,Luke Wilson,INC0000000192,admin,Australia,Luke Wilson,Don Goodliffe,2023-06-18 08:09:47.068608027,Issue with Printer738 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-25 12:04:04.032337887,2023-06-14 04:06:00,Luke Wilson,INC0000000193,system,Australia,Charlie Whitherspoon,Bud Richman,2023-06-25 12:04:04.032337887,Printer546 is malfunctioning,Hardware,2 - High
|
||||
Inquiry / Help,Closed,2023-06-29 12:44:20.753397452,2023-06-15 07:56:00,Luke Wilson,INC0000000194,employee,United States,Beth Anglin,David Loo,2023-06-29 12:44:20.753397452,Need help with application usage,Service Desk,3 - Moderate
|
||||
Network,Closed,2023-06-29 19:11:43.933822843,2023-06-19 10:41:00,Howard Johnson,INC0000000195,admin,Australia,Howard Johnson,David Loo,2023-06-29 19:11:43.933822843,Connection issues with network in Australia,Network,2 - High
|
||||
Software,Closed,2023-06-28 14:05:28.538482547,2023-06-19 11:21:00,Charlie Whitherspoon,INC0000000196,admin,United States,Fred Luddy,ITIL User,2023-06-28 14:05:28.538482547,Software update failure,Software,2 - High
|
||||
Hardware,Closed,2023-06-25 09:59:55.161658018,2023-06-19 12:22:00,Howard Johnson,INC0000000197,admin,Canada,Beth Anglin,David Loo,2023-06-25 09:59:55.161658018,Server hardware malfunction on SCSI Canada1024,Hardware,2 - High
|
||||
Network,Closed,2023-06-22 02:33:39.410009617,2023-06-19 13:59:00,Fred Luddy,INC0000000198,employee,Canada,Luke Wilson,Don Goodliffe,2023-06-22 02:33:39.410009617,Slow network connection in office,Network,2 - High
|
||||
Hardware,Closed,2023-07-01 10:42:22.374438866,2023-06-21 19:04:00,Beth Anglin,INC0000000199,employee,India,Beth Anglin,David Loo,2023-07-01 10:42:22.374438866,Issues detected with server123,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-07-06 13:13:24.843387226,2023-06-22 17:34:00,Beth Anglin,INC0000000200,employee,UK,Fred Luddy,Bud Richman,2023-07-06 13:13:24.843387226,Issue with the sound driver on workstation PC8,Hardware,2 - High
|
||||
Hardware,Closed,2023-07-06 13:08:33.074641515,2023-06-23 12:47:00,Luke Wilson,INC0000000201,system,India,Beth Anglin,Bud Richman,2023-07-06 13:08:33.074641515,Problem with Cooling Fan,Hardware,2 - High
|
||||
Hardware,Resolved,2023-06-29 22:17:23.746230748,2023-06-23 20:11:00,Howard Johnson,INC0000000202,system,Canada,Fred Luddy,ITIL User,2023-06-29 22:17:23.746230748,Hard drive failure on office desktop,Hardware,2 - High
|
||||
Database,Resolved,2023-07-06 00:00:59.012006127,2023-06-25 04:50:00,Charlie Whitherspoon,INC0000000203,employee,UK,Charlie Whitherspoon,David Loo,2023-07-06 00:00:59.012006127,Unable to connect to SQL Database,Database,2 - High
|
||||
Database,Closed,2023-07-01 09:46:55.866723604,2023-06-25 10:31:00,Howard Johnson,INC0000000204,admin,Australia,Luke Wilson,David Loo,2023-07-01 09:46:55.866723604,Database server error 550,Database,2 - High
|
||||
Hardware,Resolved,2023-07-06 12:23:47.840976362,2023-06-25 22:45:00,Howard Johnson,INC0000000205,system,Australia,Howard Johnson,Don Goodliffe,2023-07-06 12:23:47.840976362,Printer348 is not responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-30 06:01:54.007909417,2023-06-26 06:56:00,Fred Luddy,INC0000000206,employee,Australia,Charlie Whitherspoon,David Loo,2023-06-30 06:01:54.007909417,Printer546 is not responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-07-09 14:14:29.904659016,2023-06-26 08:23:00,Charlie Whitherspoon,INC0000000207,admin,United States,Fred Luddy,ITIL User,2023-07-09 14:14:29.904659016,PC1023 is not starting,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-07-06 07:29:00.832219981,2023-06-26 17:38:00,Howard Johnson,INC0000000208,admin,Australia,Fred Luddy,Bud Richman,2023-07-06 07:29:00.832219981,Printer error at Printer848,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-07-09 20:00:30.308403141,2023-06-27 10:20:00,Howard Johnson,INC0000000209,system,Australia,Luke Wilson,David Loo,2023-07-09 20:00:30.308403141,Printer546 in Sydney office is not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-06 03:31:13.838619495,2023-06-27 21:41:00,Fred Luddy,INC0000000210,admin,UK,Howard Johnson,Don Goodliffe,2023-07-06 03:31:13.838619495,Issue with server SR654,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-12 18:45:07.705046349,2023-06-28 11:02:00,Fred Luddy,INC0000000211,admin,Australia,Fred Luddy,David Loo,2023-07-12 18:45:07.705046349,Printer546 issue in Sydney office,Hardware,2 - High
|
||||
Hardware,Closed,2023-07-06 00:45:36.251942561,2023-06-28 14:35:00,Beth Anglin,INC0000000212,system,India,Howard Johnson,David Loo,2023-07-06 00:45:36.251942561,Terminal343 is not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-06-30 23:29:55.851304419,2023-06-29 19:01:00,Charlie Whitherspoon,INC0000000213,system,Australia,Luke Wilson,David Loo,2023-06-30 23:29:55.851304419,Printer error at Printer679,Hardware,3 - Moderate
|
||||
Network,Closed,2023-07-10 23:00:15.709745176,2023-07-02 07:35:00,Beth Anglin,INC0000000214,system,India,Luke Wilson,Bud Richman,2023-07-10 23:00:15.709745176,Unable to establish VPN connection,Network,2 - High
|
||||
Hardware,Closed,2023-07-14 08:23:51.080019979,2023-07-03 06:45:00,Charlie Whitherspoon,INC0000000215,system,Australia,Charlie Whitherspoon,ITIL User,2023-07-14 08:23:51.080019979,Printer546 is not working!,Hardware,1 - Critical
|
||||
Inquiry / Help,Closed,2023-07-15 22:29:05.785588895,2023-07-04 04:13:00,Luke Wilson,INC0000000216,system,UK,Charlie Whitherspoon,David Loo,2023-07-15 22:29:05.785588895,Inquiry regarding help with account access,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-07-14 06:35:05.209399792,2023-07-04 13:46:00,Fred Luddy,INC0000000217,admin,Australia,Luke Wilson,ITIL User,2023-07-14 06:35:05.209399792,Printer fail - PrinterID::Printer546,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-07-21 18:09:17.803746956,2023-07-07 04:36:00,Charlie Whitherspoon,INC0000000218,employee,India,Beth Anglin,Bud Richman,2023-07-21 18:09:17.803746956,Need assistance with software installation,Service Desk,3 - Moderate
|
||||
Inquiry / Help,Resolved,2023-07-08 04:48:00.000000000,2023-07-07 04:48:00,Beth Anglin,INC0000000219,system,UK,Howard Johnson,David Loo,2023-07-08 04:48:00.000000000,Assistance needed with UK enquiry system,Service Desk,3 - Moderate
|
||||
Network,Closed,2023-07-11 23:34:29.390060680,2023-07-07 05:58:00,Howard Johnson,INC0000000220,employee,UK,Howard Johnson,David Loo,2023-07-11 23:34:29.390060680,Internet connectivity issue in UK office,Network,2 - High
|
||||
Hardware,Resolved,2023-07-13 16:19:33.649785697,2023-07-07 20:52:00,Charlie Whitherspoon,INC0000000221,employee,Canada,Beth Anglin,Bud Richman,2023-07-13 16:19:33.649785697,Server malfunction in Canada,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-07-23 19:05:17.533035942,2023-07-08 00:31:00,Luke Wilson,INC0000000222,employee,Australia,Charlie Whitherspoon,ITIL User,2023-07-23 19:05:17.533035942,Printer malfunction detected on Printer546,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-25 03:32:18.462401146,2023-07-09 03:39:00,Beth Anglin,INC0000000223,system,India,Howard Johnson,Bud Richman,2023-07-25 03:32:18.462401146,Computer not starting,Hardware,2 - High
|
||||
Hardware,Closed,2023-07-26 17:51:52.767336231,2023-07-11 16:02:00,Luke Wilson,INC0000000224,admin,India,Beth Anglin,David Loo,2023-07-26 17:51:52.767336231,Laptop malfunction with ID233,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-20 02:37:10.813279462,2023-07-11 18:42:00,Howard Johnson,INC0000000225,employee,UK,Howard Johnson,Bud Richman,2023-07-20 02:37:10.813279462,Phone681 is not responding,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-27 12:30:15.368139102,2023-07-12 00:27:00,Charlie Whitherspoon,INC0000000226,admin,Australia,Howard Johnson,Bud Richman,2023-07-27 12:30:15.368139102,Printer422 is malfunctioning in Australia,Hardware,2 - High
|
||||
Software,Closed,2023-07-19 14:54:49.403388921,2023-07-12 03:22:00,Luke Wilson,INC0000000227,employee,Australia,Beth Anglin,Don Goodliffe,2023-07-19 14:54:49.403388921,Error message on Software ABC,Software,3 - Moderate
|
||||
Hardware,Resolved,2023-07-14 21:15:31.584390518,2023-07-12 05:50:00,Fred Luddy,INC0000000228,system,Australia,Fred Luddy,Don Goodliffe,2023-07-14 21:15:31.584390518,Printer546 is not working,Hardware,1 - Critical
|
||||
Software,Resolved,2023-07-28 21:34:00.908307924,2023-07-14 21:27:00,Howard Johnson,INC0000000229,system,UK,Beth Anglin,ITIL User,2023-07-28 21:34:00.908307924,Slowness in running application XYZ,Software,3 - Moderate
|
||||
Hardware,Resolved,2023-07-20 09:47:52.908236069,2023-07-15 12:17:00,Fred Luddy,INC0000000230,admin,Australia,Beth Anglin,ITIL User,2023-07-20 09:47:52.908236069,Issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-07-25 03:53:49.978687958,2023-07-15 14:09:00,Fred Luddy,INC0000000231,admin,Australia,Howard Johnson,Bud Richman,2023-07-25 03:53:49.978687958,Printer546 is not working properly.,Hardware,2 - High
|
||||
Hardware,Resolved,2023-07-22 05:03:13.822074451,2023-07-16 10:58:00,Beth Anglin,INC0000000232,employee,Australia,Beth Anglin,Don Goodliffe,2023-07-22 05:03:13.822074451,Printer783 is not printing,Hardware,2 - High
|
||||
Hardware,Closed,2023-07-26 01:50:52.068385147,2023-07-19 12:03:00,Luke Wilson,INC0000000233,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-07-26 01:50:52.068385147,Printer786 is not working,Hardware,1 - Critical
|
||||
Inquiry / Help,Closed,2023-08-01 00:45:22.754376187,2023-07-20 12:12:00,Fred Luddy,INC0000000234,system,United States,Luke Wilson,Bud Richman,2023-08-01 00:45:22.754376187,Inquiry about missing functionalities,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-07-23 16:38:46.547163481,2023-07-21 07:12:00,Howard Johnson,INC0000000235,admin,UK,Charlie Whitherspoon,Bud Richman,2023-07-23 16:38:46.547163481,Problem with office PC,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-07-25 23:13:55.734880101,2023-07-22 04:13:00,Charlie Whitherspoon,INC0000000236,employee,Australia,Beth Anglin,Don Goodliffe,2023-07-25 23:13:55.734880101,Printer546 - Not Printing is causing issues,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-07-30 12:07:08.997531852,2023-07-22 16:29:00,Fred Luddy,INC0000000237,admin,United States,Charlie Whitherspoon,Bud Richman,2023-07-30 12:07:08.997531852,Need assistance with software installation,Service Desk,3 - Moderate
|
||||
Inquiry / Help,Closed,2023-08-09 03:19:49.048508681,2023-07-22 22:39:00,Howard Johnson,INC0000000238,employee,United States,Beth Anglin,Bud Richman,2023-08-09 03:19:49.048508681,Need help with software installation,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-08-04 04:46:10.581661057,2023-07-22 23:48:00,Howard Johnson,INC0000000239,admin,Australia,Beth Anglin,Bud Richman,2023-08-04 04:46:10.581661057,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Network,Closed,2023-07-31 00:33:13.437296119,2023-07-23 08:50:00,Beth Anglin,INC0000000240,admin,Australia,Howard Johnson,Don Goodliffe,2023-07-31 00:33:13.437296119,Network router behaving erratically,Network,3 - Moderate
|
||||
Network,Resolved,2023-07-25 09:09:00.000000000,2023-07-24 09:09:00,Howard Johnson,INC0000000241,admin,India,Luke Wilson,Don Goodliffe,2023-07-25 09:09:00.000000000,Slow internet connectivity issues,Network,3 - Moderate
|
||||
Network,Resolved,2023-07-31 04:33:45.303280610,2023-07-24 09:13:00,Beth Anglin,INC0000000242,admin,UK,Charlie Whitherspoon,Don Goodliffe,2023-07-31 04:33:45.303280610,Unable to connect to VPN,Network,2 - High
|
||||
Hardware,Resolved,2023-08-11 14:17:08.329647264,2023-07-25 08:08:00,Beth Anglin,INC0000000243,system,Canada,Fred Luddy,David Loo,2023-08-11 14:17:08.329647264,Motherboard failure on company desktop,Hardware,2 - High
|
||||
Software,Closed,2023-08-02 12:16:09.558065832,2023-07-25 20:00:00,Howard Johnson,INC0000000244,admin,Australia,Charlie Whitherspoon,ITIL User,2023-08-02 12:16:09.558065832,Error 404 on Software789,Software,2 - High
|
||||
Inquiry / Help,Closed,2023-08-12 18:27:47.789421821,2023-07-26 06:53:00,Charlie Whitherspoon,INC0000000245,admin,Canada,Charlie Whitherspoon,Bud Richman,2023-08-12 18:27:47.789421821,Need assistance with software installation,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-08-02 04:32:04.560171158,2023-07-26 17:09:00,Beth Anglin,INC0000000246,system,Australia,Howard Johnson,David Loo,2023-08-02 04:32:04.560171158,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-07 06:13:36.125124469,2023-07-27 04:40:00,Howard Johnson,INC0000000247,system,Australia,Luke Wilson,Don Goodliffe,2023-08-07 06:13:36.125124469,Printer546 is not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-08 02:22:19.738512734,2023-07-27 08:18:00,Charlie Whitherspoon,INC0000000248,system,UK,Charlie Whitherspoon,Don Goodliffe,2023-08-08 02:22:19.738512734,UK laptop malfunctioning,Hardware,2 - High
|
||||
Network,Closed,2023-08-08 20:25:25.735379947,2023-07-27 19:21:00,Fred Luddy,INC0000000249,system,United States,Beth Anglin,Bud Richman,2023-08-08 20:25:25.735379947,Network outage in United States office,Network,1 - Critical
|
||||
Inquiry / Help,Closed,2023-08-04 22:43:36.252615961,2023-07-28 19:29:00,Charlie Whitherspoon,INC0000000250,admin,Canada,Luke Wilson,ITIL User,2023-08-04 22:43:36.252615961,Need assistance with VPN setup,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-08-07 18:20:23.668968040,2023-07-29 10:15:00,Luke Wilson,INC0000000251,employee,Australia,Luke Wilson,Don Goodliffe,2023-08-07 18:20:23.668968040,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-02 05:21:25.991706511,2023-07-29 11:39:00,Charlie Whitherspoon,INC0000000252,employee,Australia,Howard Johnson,ITIL User,2023-08-02 05:21:25.991706511,Printer error on Printer546,Hardware,2 - High
|
||||
Software,Closed,2023-08-03 13:35:26.483807424,2023-07-30 09:49:00,Beth Anglin,INC0000000253,admin,Australia,Beth Anglin,David Loo,2023-08-03 13:35:26.483807424,MySQL database connection error on server AU120,Software,2 - High
|
||||
Software,Closed,2023-08-11 16:02:37.684405305,2023-07-30 09:52:00,Fred Luddy,INC0000000254,employee,Australia,Charlie Whitherspoon,ITIL User,2023-08-11 16:02:37.684405305,Unable to update software on local machine,Software,2 - High
|
||||
Hardware,Closed,2023-08-13 06:44:24.424288482,2023-07-30 17:08:00,Charlie Whitherspoon,INC0000000255,employee,UK,Beth Anglin,ITIL User,2023-08-13 06:44:24.424288482,Broken server needs replacement,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-02 05:44:00.000000000,2023-08-01 05:44:00,Fred Luddy,INC0000000256,employee,Australia,Beth Anglin,ITIL User,2023-08-02 05:44:00.000000000,Printer546 is not functioning correctly,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-07 18:21:44.544015394,2023-08-01 11:34:00,Beth Anglin,INC0000000257,employee,Australia,Luke Wilson,David Loo,2023-08-07 18:21:44.544015394,Printer322 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-10 22:01:38.300764703,2023-08-01 13:22:00,Beth Anglin,INC0000000258,employee,Australia,Luke Wilson,David Loo,2023-08-10 22:01:38.300764703,Printer420 is not responding,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-08-09 07:59:01.326042131,2023-08-02 07:41:00,Luke Wilson,INC0000000259,admin,Australia,Howard Johnson,Bud Richman,2023-08-09 07:59:01.326042131,Printer327 has stopped working in Australia,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-08-05 08:06:09.262346306,2023-08-02 14:46:00,Luke Wilson,INC0000000260,employee,India,Charlie Whitherspoon,Don Goodliffe,2023-08-05 08:06:09.262346306,Monitor display intermittent failure,Hardware,2 - High
|
||||
Network,Closed,2023-08-04 08:39:00.000000000,2023-08-03 08:39:00,Fred Luddy,INC0000000261,employee,India,Howard Johnson,Don Goodliffe,2023-08-04 08:39:00.000000000,Unable to connect to server,Network,2 - High
|
||||
Hardware,Resolved,2023-08-17 04:45:43.185739101,2023-08-03 16:41:00,Beth Anglin,INC0000000262,employee,Australia,Beth Anglin,Don Goodliffe,2023-08-17 04:45:43.185739101,Printer546 is not printing in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-04 18:56:00.000000000,2023-08-03 18:56:00,Fred Luddy,INC0000000263,employee,Australia,Luke Wilson,Don Goodliffe,2023-08-04 18:56:00.000000000,Printer789 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-14 09:36:00.295254938,2023-08-05 07:39:00,Howard Johnson,INC0000000264,admin,Australia,Charlie Whitherspoon,ITIL User,2023-08-14 09:36:00.295254938,Printer error at Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-07 09:44:00.000000000,2023-08-06 09:44:00,Beth Anglin,INC0000000265,employee,Australia,Howard Johnson,Bud Richman,2023-08-07 09:44:00.000000000,Printer546 in Australia location has a paper jam,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-15 14:03:57.099952165,2023-08-07 02:37:00,Howard Johnson,INC0000000266,employee,Australia,Charlie Whitherspoon,ITIL User,2023-08-15 14:03:57.099952165,Printer467 is out of service!,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-15 21:18:27.557222760,2023-08-08 09:29:00,Charlie Whitherspoon,INC0000000267,admin,Australia,Howard Johnson,Don Goodliffe,2023-08-15 21:18:27.557222760,Issue with Printer546 in Australia,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-08-28 16:17:39.682367202,2023-08-09 04:10:00,Charlie Whitherspoon,INC0000000268,admin,Australia,Howard Johnson,Don Goodliffe,2023-08-28 16:17:39.682367202,Printer678 issue in Australia,Hardware,2 - High
|
||||
Network,Closed,2023-08-10 06:30:11.927271898,2023-08-09 04:13:00,Charlie Whitherspoon,INC0000000269,admin,United States,Howard Johnson,ITIL User,2023-08-10 06:30:11.927271898,Unable to connect to VPN,Network,2 - High
|
||||
Hardware,Closed,2023-08-19 00:17:36.874122878,2023-08-09 17:08:00,Luke Wilson,INC0000000270,system,Australia,Fred Luddy,Bud Richman,2023-08-19 00:17:36.874122878,Printer789 is not responding,Hardware,1 - Critical
|
||||
Inquiry / Help,Closed,2023-08-20 02:50:00.481014067,2023-08-10 08:09:00,Fred Luddy,INC0000000271,employee,UK,Fred Luddy,ITIL User,2023-08-20 02:50:00.481014067,Need assistance with IT inquiry,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-08-24 03:39:08.172397201,2023-08-11 04:15:00,Luke Wilson,INC0000000272,employee,Australia,Fred Luddy,ITIL User,2023-08-24 03:39:08.172397201,Printer546 is not printing in Australia,Hardware,1 - Critical
|
||||
Software,Closed,2023-08-13 06:41:18.526534971,2023-08-11 09:04:00,Charlie Whitherspoon,INC0000000273,admin,UK,Beth Anglin,David Loo,2023-08-13 06:41:18.526534971,Software Issue: Error while loading application on VM,Software,3 - Moderate
|
||||
Hardware,Closed,2023-08-18 23:36:22.191419882,2023-08-12 03:28:00,Luke Wilson,INC0000000274,system,Australia,Charlie Whitherspoon,David Loo,2023-08-18 23:36:22.191419882,Printer546 is not working,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-08-13 15:36:00.000000000,2023-08-12 15:36:00,Howard Johnson,INC0000000275,admin,Australia,Fred Luddy,Bud Richman,2023-08-13 15:36:00.000000000,Printer546 is not functioning in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-19 05:22:39.220366354,2023-08-13 08:04:00,Luke Wilson,INC0000000276,system,Australia,Beth Anglin,Don Goodliffe,2023-08-19 05:22:39.220366354,Printer546 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-08-17 17:26:24.730681330,2023-08-14 09:58:00,Howard Johnson,INC0000000277,system,Australia,Beth Anglin,ITIL User,2023-08-17 17:26:24.730681330,Printer546 Error in Australia,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-08-21 08:48:34.847604103,2023-08-15 20:03:00,Beth Anglin,INC0000000278,employee,Australia,Fred Luddy,ITIL User,2023-08-21 08:48:34.847604103,Printer546 is malfunctioning,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-08-22 04:24:28.410211673,2023-08-16 19:14:00,Fred Luddy,INC0000000279,system,Australia,Beth Anglin,Don Goodliffe,2023-08-22 04:24:28.410211673,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-25 09:49:36.214757254,2023-08-17 11:38:00,Luke Wilson,INC0000000280,admin,Australia,Charlie Whitherspoon,David Loo,2023-08-25 09:49:36.214757254,Printer issue with Printer546,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-08-19 10:57:00.000000000,2023-08-18 10:57:00,Fred Luddy,INC0000000281,admin,Australia,Charlie Whitherspoon,ITIL User,2023-08-19 10:57:00.000000000,Issue with Printer546,Hardware,1 - Critical
|
||||
Database,Closed,2023-08-23 19:40:27.266944172,2023-08-19 12:57:00,Howard Johnson,INC0000000282,system,UK,Howard Johnson,Bud Richman,2023-08-23 19:40:27.266944172,Connection issue with UK_DB47 Database,Database,2 - High
|
||||
Hardware,Closed,2023-08-24 00:59:37.177124394,2023-08-20 08:09:00,Charlie Whitherspoon,INC0000000283,employee,Australia,Howard Johnson,David Loo,2023-08-24 00:59:37.177124394,Printer678 malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-08-26 09:29:01.912064757,2023-08-20 08:38:00,Fred Luddy,INC0000000284,employee,Australia,Beth Anglin,ITIL User,2023-08-26 09:29:01.912064757,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-08-24 03:28:09.603360173,2023-08-20 18:16:00,Luke Wilson,INC0000000285,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-08-24 03:28:09.603360173,Printer546 is failing,Hardware,1 - Critical
|
||||
Inquiry / Help,Closed,2023-08-26 15:36:56.981753690,2023-08-21 07:17:00,Howard Johnson,INC0000000286,admin,India,Fred Luddy,Bud Richman,2023-08-26 15:36:56.981753690,Need help with software installation,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-09-05 02:07:43.113682721,2023-08-21 07:38:00,Howard Johnson,INC0000000287,admin,United States,Howard Johnson,David Loo,2023-09-05 02:07:43.113682721,Hardware issue with the CPU overheating,Hardware,1 - Critical
|
||||
Database,Resolved,2023-08-22 19:11:00.000000000,2023-08-21 19:11:00,Charlie Whitherspoon,INC0000000288,employee,UK,Beth Anglin,David Loo,2023-08-22 19:11:00.000000000,Error detected in Database system,Database,1 - Critical
|
||||
Network,Closed,2023-08-26 16:09:36.942939516,2023-08-22 03:19:00,Fred Luddy,INC0000000289,admin,Australia,Beth Anglin,Bud Richman,2023-08-26 16:09:36.942939516,"Network connection issue, Australia",Network,2 - High
|
||||
Hardware,Closed,2023-08-25 07:22:00.000000000,2023-08-24 07:22:00,Luke Wilson,INC0000000290,admin,Australia,Charlie Whitherspoon,David Loo,2023-08-25 07:22:00.000000000,Printer546 is offline,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-03 18:58:33.803118210,2023-08-25 12:10:00,Fred Luddy,INC0000000291,admin,Australia,Howard Johnson,David Loo,2023-09-03 18:58:33.803118210,Printer489 has a paper jam,Hardware,2 - High
|
||||
Network,Closed,2023-08-30 14:14:08.283969923,2023-08-25 22:41:00,Charlie Whitherspoon,INC0000000292,employee,UK,Luke Wilson,Bud Richman,2023-08-30 14:14:08.283969923,Losing connectivity frequently,Network,2 - High
|
||||
Database,Resolved,2023-08-27 03:03:00.000000000,2023-08-26 03:03:00,Beth Anglin,INC0000000293,admin,UK,Luke Wilson,ITIL User,2023-08-27 03:03:00.000000000,Issue with SQL Server783,Database,2 - High
|
||||
Database,Closed,2023-08-30 13:49:54.648995923,2023-08-27 21:14:00,Fred Luddy,INC0000000294,system,Canada,Fred Luddy,Don Goodliffe,2023-08-30 13:49:54.648995923,Unable to connect to Database DB4434,Database,2 - High
|
||||
Database,Closed,2023-09-01 14:19:29.165134519,2023-08-28 16:06:00,Fred Luddy,INC0000000295,employee,Australia,Luke Wilson,ITIL User,2023-09-01 14:19:29.165134519,Database error on SQL server,Database,2 - High
|
||||
Hardware,Closed,2023-08-31 15:44:00.000000000,2023-08-30 15:44:00,Fred Luddy,INC0000000296,system,Australia,Howard Johnson,ITIL User,2023-08-31 15:44:00.000000000,Printer546 is not working,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-09-07 23:02:26.238518168,2023-08-31 14:06:00,Luke Wilson,INC0000000297,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-09-07 23:02:26.238518168,Printer392 is constantly jamming,Hardware,2 - High
|
||||
Hardware,Resolved,2023-09-07 09:17:08.466232846,2023-09-02 01:15:00,Beth Anglin,INC0000000298,employee,Australia,Fred Luddy,ITIL User,2023-09-07 09:17:08.466232846,Printer786 malfunctioning,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-09-03 02:19:00.000000000,2023-09-02 02:19:00,Howard Johnson,INC0000000299,employee,India,Luke Wilson,David Loo,2023-09-03 02:19:00.000000000,Issue with motherboard in workstation DSK789,Hardware,2 - High
|
||||
Software,Resolved,2023-09-11 18:47:05.423433239,2023-09-02 03:20:00,Luke Wilson,INC0000000300,system,UK,Fred Luddy,Bud Richman,2023-09-11 18:47:05.423433239,Error with billing software,Software,2 - High
|
||||
Hardware,Closed,2023-09-14 16:03:56.241021820,2023-09-03 07:56:00,Charlie Whitherspoon,INC0000000301,system,Australia,Howard Johnson,ITIL User,2023-09-14 16:03:56.241021820,Printer567 is not responding,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-13 05:14:51.293532720,2023-09-03 12:21:00,Charlie Whitherspoon,INC0000000302,employee,Australia,Fred Luddy,Bud Richman,2023-09-13 05:14:51.293532720,Printer546 - printing issue in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-13 13:44:32.848563603,2023-09-03 15:48:00,Fred Luddy,INC0000000303,system,India,Howard Johnson,David Loo,2023-09-13 13:44:32.848563603,Bad sector on Hard Disk in desktop model ID E0395,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-09-16 03:46:06.609216047,2023-09-04 16:00:00,Fred Luddy,INC0000000304,admin,Australia,Luke Wilson,Bud Richman,2023-09-16 03:46:06.609216047,Printer failure on Printer546,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-09-15 01:07:41.823641770,2023-09-04 18:36:00,Luke Wilson,INC0000000305,system,UK,Charlie Whitherspoon,ITIL User,2023-09-15 01:07:41.823641770,Issue with desktop workstation 3F45,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-13 23:47:44.874072603,2023-09-06 12:24:00,Luke Wilson,INC0000000306,admin,United States,Charlie Whitherspoon,Don Goodliffe,2023-09-13 23:47:44.874072603,Server123 has crashed,Hardware,1 - Critical
|
||||
Network,Resolved,2023-09-13 04:40:34.280556892,2023-09-07 03:40:00,Howard Johnson,INC0000000307,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-09-13 04:40:34.280556892,Network outage in Australia,Network,1 - Critical
|
||||
Database,Resolved,2023-09-20 04:57:50.969058643,2023-09-07 17:44:00,Luke Wilson,INC0000000308,employee,United States,Charlie Whitherspoon,ITIL User,2023-09-20 04:57:50.969058643,Unable to access client database on Oracle,Database,2 - High
|
||||
Hardware,Resolved,2023-09-13 00:09:56.020598186,2023-09-08 07:01:00,Luke Wilson,INC0000000309,system,Australia,Howard Johnson,Bud Richman,2023-09-13 00:09:56.020598186,Printer546 in Australia is not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-09-13 01:47:32.680016461,2023-09-08 17:14:00,Howard Johnson,INC0000000310,admin,Canada,Beth Anglin,Bud Richman,2023-09-13 01:47:32.680016461,Workstation CPU Overheating,Hardware,2 - High
|
||||
Hardware,Resolved,2023-09-17 17:36:33.312203552,2023-09-11 18:57:00,Beth Anglin,INC0000000311,admin,Australia,Beth Anglin,Don Goodliffe,2023-09-17 17:36:33.312203552,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Database,Closed,2023-09-22 23:55:17.560026848,2023-09-12 15:43:00,Luke Wilson,INC0000000312,system,United States,Howard Johnson,Don Goodliffe,2023-09-22 23:55:17.560026848,Database Error ID382 - System Slowdown,Database,2 - High
|
||||
Database,Closed,2023-09-18 08:09:07.717459166,2023-09-13 03:14:00,Howard Johnson,INC0000000313,system,UK,Charlie Whitherspoon,ITIL User,2023-09-18 08:09:07.717459166,Error loading Database267 in the UK office,Database,2 - High
|
||||
Hardware,Resolved,2023-09-18 19:43:08.637023310,2023-09-13 23:38:00,Luke Wilson,INC0000000314,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-09-18 19:43:08.637023310,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-28 01:25:23.074702845,2023-09-14 10:29:00,Charlie Whitherspoon,INC0000000315,admin,Canada,Luke Wilson,Bud Richman,2023-09-28 01:25:23.074702845,Server Crash on machine233,Hardware,1 - Critical
|
||||
Database,Closed,2023-09-16 11:23:21.172473709,2023-09-14 10:58:00,Fred Luddy,INC0000000316,admin,Australia,Howard Johnson,Bud Richman,2023-09-16 11:23:21.172473709,Error in Database connectivity,Database,2 - High
|
||||
Hardware,Closed,2023-09-21 01:28:06.686513687,2023-09-14 13:00:00,Luke Wilson,INC0000000317,admin,Australia,Charlie Whitherspoon,ITIL User,2023-09-21 01:28:06.686513687,Printer546 issue in Australia,Hardware,2 - High
|
||||
Inquiry / Help,Closed,2023-09-15 15:54:00.000000000,2023-09-14 15:54:00,Beth Anglin,INC0000000318,employee,Australia,Luke Wilson,David Loo,2023-09-15 15:54:00.000000000,Printer546 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-09-23 18:17:16.716938240,2023-09-15 05:28:00,Charlie Whitherspoon,INC0000000319,system,Australia,Charlie Whitherspoon,Don Goodliffe,2023-09-23 18:17:16.716938240,Printer546 in Australia experiencing issues,Hardware,1 - Critical
|
||||
Software,Closed,2023-09-20 20:58:46.751153395,2023-09-16 09:18:00,Fred Luddy,INC0000000320,system,Canada,Fred Luddy,David Loo,2023-09-20 20:58:46.751153395,Performance issues with Software123,Software,3 - Moderate
|
||||
Hardware,Closed,2023-09-22 13:11:16.460337579,2023-09-16 09:20:00,Fred Luddy,INC0000000321,admin,Australia,Howard Johnson,ITIL User,2023-09-22 13:11:16.460337579,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-30 16:57:13.076218110,2023-09-18 03:29:00,Charlie Whitherspoon,INC0000000322,employee,United States,Beth Anglin,Don Goodliffe,2023-09-30 16:57:13.076218110,Server failure for US node,Hardware,1 - Critical
|
||||
Network,Closed,2023-09-22 15:44:42.056556368,2023-09-19 03:45:00,Beth Anglin,INC0000000323,employee,United States,Luke Wilson,Bud Richman,2023-09-22 15:44:42.056556368,Internet connection problem in the United States,Network,2 - High
|
||||
Hardware,Closed,2023-10-05 10:37:47.028545047,2023-09-19 08:44:00,Fred Luddy,INC0000000324,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-10-05 10:37:47.028545047,Printer546 is having issues,Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-03 08:14:22.675801438,2023-09-20 06:22:00,Luke Wilson,INC0000000325,system,Australia,Charlie Whitherspoon,Don Goodliffe,2023-10-03 08:14:22.675801438,The Printer546 in Australia is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-22 07:03:43.180128742,2023-09-20 20:36:00,Luke Wilson,INC0000000326,system,United States,Charlie Whitherspoon,David Loo,2023-09-22 07:03:43.180128742,Server832 failure at the data center,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-10-05 14:06:46.265722782,2023-09-21 05:32:00,Beth Anglin,INC0000000327,admin,United States,Charlie Whitherspoon,Don Goodliffe,2023-10-05 14:06:46.265722782,Monitor345 not functioning,Hardware,2 - High
|
||||
Hardware,Resolved,2023-09-28 10:20:40.858388369,2023-09-21 14:49:00,Howard Johnson,INC0000000328,employee,UK,Luke Wilson,Don Goodliffe,2023-09-28 10:20:40.858388369,Issue with loading up laptop model L-914,Hardware,2 - High
|
||||
Network,Closed,2023-09-22 21:14:00.000000000,2023-09-21 21:14:00,Beth Anglin,INC0000000329,system,India,Howard Johnson,Don Goodliffe,2023-09-22 21:14:00.000000000,Network connectivity issue,Network,3 - Moderate
|
||||
Hardware,Closed,2023-10-06 07:39:16.761597562,2023-09-21 23:53:00,Fred Luddy,INC0000000330,system,UK,Charlie Whitherspoon,David Loo,2023-10-06 07:39:16.761597562,Failed Hard Disk Drive on WorkstationUK786,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-05 14:59:20.783399866,2023-09-24 10:56:00,Fred Luddy,INC0000000331,system,Canada,Charlie Whitherspoon,David Loo,2023-10-05 14:59:20.783399866,Desktop PC not starting up,Hardware,2 - High
|
||||
Database,Closed,2023-10-03 08:53:36.796709606,2023-09-24 13:55:00,Howard Johnson,INC0000000332,employee,UK,Fred Luddy,David Loo,2023-10-03 08:53:36.796709606,Error detected in Database6543,Database,2 - High
|
||||
Hardware,Closed,2023-09-26 02:31:00.000000000,2023-09-25 02:31:00,Beth Anglin,INC0000000333,admin,Australia,Beth Anglin,David Loo,2023-09-26 02:31:00.000000000,Printer768 in Australia is malfunctioning,Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-02 19:11:48.163774968,2023-09-25 19:21:00,Luke Wilson,INC0000000334,employee,Australia,Charlie Whitherspoon,David Loo,2023-10-02 19:11:48.163774968,Printer988 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-09-26 20:40:00.000000000,2023-09-25 20:40:00,Howard Johnson,INC0000000335,employee,Australia,Luke Wilson,ITIL User,2023-09-26 20:40:00.000000000,Printer001 is not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-09-28 19:04:36.066288946,2023-09-25 20:56:00,Luke Wilson,INC0000000336,admin,Australia,Howard Johnson,Don Goodliffe,2023-09-28 19:04:36.066288946,Printer546 is not printing in Australia,Hardware,1 - Critical
|
||||
Database,Resolved,2023-10-01 22:54:51.030860190,2023-09-26 00:01:00,Luke Wilson,INC0000000337,admin,India,Howard Johnson,David Loo,2023-10-01 22:54:51.030860190,Database Server Instance Performance Issue,Database,2 - High
|
||||
Hardware,Closed,2023-09-27 06:11:00.000000000,2023-09-26 06:11:00,Charlie Whitherspoon,INC0000000338,system,United States,Charlie Whitherspoon,David Loo,2023-09-27 06:11:00.000000000,Desktop PC524 sudden shutdown,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-02 04:18:49.766694358,2023-09-27 00:04:00,Charlie Whitherspoon,INC0000000339,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2023-10-02 04:18:49.766694358,Printer855 is not functioning properly,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-10-10 20:23:44.798714886,2023-09-27 23:08:00,Howard Johnson,INC0000000340,system,India,Charlie Whitherspoon,Don Goodliffe,2023-10-10 20:23:44.798714886,Inquiry about software installation,Service Desk,2 - High
|
||||
Network,Closed,2023-10-02 20:53:30.682718416,2023-09-28 01:57:00,Charlie Whitherspoon,INC0000000341,admin,India,Luke Wilson,Don Goodliffe,2023-10-02 20:53:30.682718416,Network connectivity issue in India,Network,2 - High
|
||||
Hardware,Resolved,2023-10-12 20:28:38.706928972,2023-09-28 13:16:00,Beth Anglin,INC0000000342,system,UK,Luke Wilson,David Loo,2023-10-12 20:28:38.706928972,Hard drive failure on Desktop577,Hardware,2 - High
|
||||
Hardware,Resolved,2023-09-30 03:37:15.453671612,2023-09-29 02:59:00,Fred Luddy,INC0000000343,admin,Australia,Luke Wilson,Don Goodliffe,2023-09-30 03:37:15.453671612,Printer issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-10-08 04:30:10.854895991,2023-09-30 07:14:00,Howard Johnson,INC0000000344,employee,Australia,Beth Anglin,ITIL User,2023-10-08 04:30:10.854895991,Printer546 is not working properly,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-10-01 17:15:12.850603255,2023-09-30 14:37:00,Luke Wilson,INC0000000345,admin,Australia,Fred Luddy,David Loo,2023-10-01 17:15:12.850603255,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Inquiry / Help,Closed,2023-10-10 07:28:02.225169681,2023-10-01 18:20:00,Luke Wilson,INC0000000346,system,India,Beth Anglin,ITIL User,2023-10-10 07:28:02.225169681,Inquiry on Software Installation Guide,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-10-11 11:31:18.203048562,2023-10-01 20:05:00,Luke Wilson,INC0000000347,admin,Australia,Charlie Whitherspoon,ITIL User,2023-10-11 11:31:18.203048562,Printer324 in Australia is not functioning properly,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-10-03 01:05:00.000000000,2023-10-02 01:05:00,Howard Johnson,INC0000000348,admin,Australia,Beth Anglin,David Loo,2023-10-03 01:05:00.000000000,Issue with Printer874 in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-16 11:37:53.443608066,2023-10-02 20:23:00,Luke Wilson,INC0000000349,employee,United States,Luke Wilson,Don Goodliffe,2023-10-16 11:37:53.443608066,Hard drive failure on workstation,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-08 05:16:54.012585598,2023-10-03 07:12:00,Fred Luddy,INC0000000350,employee,Australia,Howard Johnson,Bud Richman,2023-10-08 05:16:54.012585598,Printer546 has a paper jam,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-10-07 15:02:09.446606967,2023-10-03 08:47:00,Beth Anglin,INC0000000351,admin,Australia,Fred Luddy,Bud Richman,2023-10-07 15:02:09.446606967,Printer error with Printer7 (Australia),Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-10 07:54:06.270667738,2023-10-03 11:29:00,Beth Anglin,INC0000000352,admin,Australia,Charlie Whitherspoon,David Loo,2023-10-10 07:54:06.270667738,Printer436 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-07 04:57:07.243553547,2023-10-03 14:29:00,Howard Johnson,INC0000000353,system,Australia,Howard Johnson,David Loo,2023-10-07 04:57:07.243553547,Printer899 is not functioning properly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-10-11 14:36:28.517445744,2023-10-04 21:50:00,Howard Johnson,INC0000000354,admin,Australia,Fred Luddy,ITIL User,2023-10-11 14:36:28.517445744,Printer damage issue with Printer7,Hardware,2 - High
|
||||
Database,Closed,2023-10-08 21:29:55.684306333,2023-10-05 06:17:00,Beth Anglin,INC0000000355,employee,UK,Luke Wilson,ITIL User,2023-10-08 21:29:55.684306333,Unable to establish connection to Database354,Database,2 - High
|
||||
Database,Resolved,2023-10-11 12:19:52.993926660,2023-10-05 12:36:00,Luke Wilson,INC0000000356,employee,India,Charlie Whitherspoon,Bud Richman,2023-10-11 12:19:52.993926660,Unable to connect to Database Server DB204,Database,1 - Critical
|
||||
Hardware,Closed,2023-10-09 10:00:05.170032833,2023-10-06 14:02:00,Beth Anglin,INC0000000357,admin,UK,Fred Luddy,David Loo,2023-10-09 10:00:05.170032833,Mouse malfunction in UK,Hardware,3 - Moderate
|
||||
Software,Resolved,2023-10-10 08:31:35.983826188,2023-10-07 19:05:00,Luke Wilson,INC0000000358,employee,UK,Howard Johnson,Bud Richman,2023-10-10 08:31:35.983826188,Software crash on workstation UK567,Software,2 - High
|
||||
Software,Resolved,2023-10-09 09:10:00.000000000,2023-10-08 09:10:00,Fred Luddy,INC0000000359,employee,Canada,Charlie Whitherspoon,ITIL User,2023-10-09 09:10:00.000000000,Cannot install the latest software update,Software,3 - Moderate
|
||||
Software,Resolved,2023-10-18 20:50:35.137819655,2023-10-08 10:24:00,Beth Anglin,INC0000000360,admin,Canada,Fred Luddy,Bud Richman,2023-10-18 20:50:35.137819655,Unable to update the database in Accounting Software,Software,3 - Moderate
|
||||
Software,Resolved,2023-10-24 10:20:11.744461099,2023-10-09 03:23:00,Beth Anglin,INC0000000361,system,Australia,Howard Johnson,Don Goodliffe,2023-10-24 10:20:11.744461099,An issue with Software56 in Australia,Software,3 - Moderate
|
||||
Software,Resolved,2023-10-16 08:15:55.096916626,2023-10-09 19:05:00,Charlie Whitherspoon,INC0000000362,employee,India,Fred Luddy,David Loo,2023-10-16 08:15:55.096916626,Software update failed on station456,Software,2 - High
|
||||
Hardware,Closed,2023-10-18 13:41:25.546404143,2023-10-10 09:43:00,Luke Wilson,INC0000000363,system,India,Fred Luddy,Bud Richman,2023-10-18 13:41:25.546404143,Monitor123 is not displaying correctly,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-14 07:51:41.450443462,2023-10-11 11:15:00,Beth Anglin,INC0000000364,admin,UK,Beth Anglin,Bud Richman,2023-10-14 07:51:41.450443462,Keyboard malfunction for desktop system D255X,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-16 08:25:44.792483730,2023-10-11 14:14:00,Luke Wilson,INC0000000365,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2023-10-16 08:25:44.792483730,The printer Printer546 in Australia has a paper jam,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-10-12 19:50:00.000000000,2023-10-11 19:50:00,Charlie Whitherspoon,INC0000000366,admin,Canada,Luke Wilson,Don Goodliffe,2023-10-12 19:50:00.000000000,Unresolved inquiry regarding software assistance,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-10-14 14:32:43.560111853,2023-10-12 01:24:00,Charlie Whitherspoon,INC0000000367,admin,United States,Luke Wilson,Don Goodliffe,2023-10-14 14:32:43.560111853,Mouse not functioning,Hardware,2 - High
|
||||
Software,Closed,2023-10-14 10:07:06.909703281,2023-10-12 03:06:00,Charlie Whitherspoon,INC0000000368,system,UK,Fred Luddy,David Loo,2023-10-14 10:07:06.909703281,Software update failed on workstation,Software,2 - High
|
||||
Hardware,Closed,2023-10-16 04:30:10.679168966,2023-10-12 20:39:00,Charlie Whitherspoon,INC0000000369,admin,India,Luke Wilson,Bud Richman,2023-10-16 04:30:10.679168966,Critical server issue with hardware,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-10-27 12:35:42.247430186,2023-10-13 09:01:00,Luke Wilson,INC0000000370,system,Australia,Luke Wilson,ITIL User,2023-10-27 12:35:42.247430186,Printer error on Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-18 21:50:20.165772966,2023-10-14 19:37:00,Fred Luddy,INC0000000371,admin,Australia,Fred Luddy,Bud Richman,2023-10-18 21:50:20.165772966,Printer error on Printer546,Hardware,2 - High
|
||||
Software,Resolved,2023-10-28 17:01:26.639036991,2023-10-15 00:02:00,Charlie Whitherspoon,INC0000000372,admin,Canada,Howard Johnson,Bud Richman,2023-10-28 17:01:26.639036991,Application XYZ failed to update,Software,2 - High
|
||||
Hardware,Resolved,2023-10-21 05:30:42.585855096,2023-10-15 00:53:00,Howard Johnson,INC0000000373,admin,Australia,Beth Anglin,Bud Richman,2023-10-21 05:30:42.585855096,Printer549 is not working in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-10-20 04:35:01.141400836,2023-10-15 17:23:00,Luke Wilson,INC0000000374,employee,United States,Howard Johnson,Bud Richman,2023-10-20 04:35:01.141400836,US Server failure,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-10-19 02:19:42.091067289,2023-10-16 14:21:00,Charlie Whitherspoon,INC0000000375,admin,Australia,Beth Anglin,ITIL User,2023-10-19 02:19:42.091067289,Printer795 malfunctioning in Sydney office,Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-21 17:05:49.162145411,2023-10-16 16:55:00,Beth Anglin,INC0000000376,admin,Australia,Beth Anglin,Bud Richman,2023-10-21 17:05:49.162145411,Printer546 is not responding,Hardware,2 - High
|
||||
Hardware,Resolved,2023-10-18 00:52:00.000000000,2023-10-17 00:52:00,Charlie Whitherspoon,INC0000000377,employee,Australia,Beth Anglin,ITIL User,2023-10-18 00:52:00.000000000,Printer failure - Printer546,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-10-25 19:50:20.337280678,2023-10-20 02:34:00,Luke Wilson,INC0000000378,admin,India,Luke Wilson,Don Goodliffe,2023-10-25 19:50:20.337280678,Unable to access application from my end,Service Desk,2 - High
|
||||
Software,Closed,2023-10-29 06:16:49.729720000,2023-10-21 03:48:00,Beth Anglin,INC0000000379,admin,India,Luke Wilson,David Loo,2023-10-29 06:16:49.729720000,Adobe Photoshop application is not responding,Software,2 - High
|
||||
Network,Closed,2023-10-30 03:46:43.553522312,2023-10-22 17:17:00,Charlie Whitherspoon,INC0000000380,system,Canada,Beth Anglin,ITIL User,2023-10-30 03:46:43.553522312,Unable to connect to network,Network,2 - High
|
||||
Database,Closed,2023-10-27 01:49:16.402277796,2023-10-23 01:08:00,Fred Luddy,INC0000000381,system,UK,Charlie Whitherspoon,Bud Richman,2023-10-27 01:49:16.402277796,Unable to connect to Database from UK location,Database,1 - Critical
|
||||
Software,Closed,2023-10-30 13:55:51.015143360,2023-10-23 14:06:00,Beth Anglin,INC0000000382,admin,India,Luke Wilson,Don Goodliffe,2023-10-30 13:55:51.015143360,Error while installing new software,Software,3 - Moderate
|
||||
Database,Resolved,2023-10-31 00:43:50.133149704,2023-10-24 02:28:00,Beth Anglin,INC0000000383,employee,United States,Beth Anglin,David Loo,2023-10-31 00:43:50.133149704,Error in SQL server update,Database,2 - High
|
||||
Hardware,Closed,2023-11-06 00:05:08.518186417,2023-10-24 20:18:00,Beth Anglin,INC0000000384,employee,Australia,Howard Johnson,ITIL User,2023-11-06 00:05:08.518186417,Printer 546 is not functioning properly in Australia,Hardware,1 - Critical
|
||||
Network,Closed,2023-11-02 19:10:09.135343415,2023-10-25 03:39:00,Luke Wilson,INC0000000385,admin,UK,Howard Johnson,ITIL User,2023-11-02 19:10:09.135343415,Cannot connect to UK server,Network,2 - High
|
||||
Hardware,Closed,2023-10-31 10:15:16.995034613,2023-10-25 05:06:00,Charlie Whitherspoon,INC0000000386,admin,Australia,Beth Anglin,Don Goodliffe,2023-10-31 10:15:16.995034613,Printer473 is not responding,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-09 09:27:41.299495824,2023-10-26 10:39:00,Howard Johnson,INC0000000387,admin,Australia,Howard Johnson,David Loo,2023-11-09 09:27:41.299495824,Printer546 is not working,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-02 21:31:23.289304071,2023-10-26 19:39:00,Beth Anglin,INC0000000388,admin,Australia,Howard Johnson,ITIL User,2023-11-02 21:31:23.289304071,Printer356 in Australia is not responding,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-09 01:36:23.094504250,2023-10-26 20:51:00,Beth Anglin,INC0000000389,admin,Australia,Fred Luddy,Bud Richman,2023-11-09 01:36:23.094504250,Printer546 has a paper jam,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-04 10:27:01.152081419,2023-10-27 23:13:00,Charlie Whitherspoon,INC0000000390,employee,Australia,Beth Anglin,David Loo,2023-11-04 10:27:01.152081419,Printer546 is not working properly,Hardware,2 - High
|
||||
Network,Resolved,2023-11-07 22:41:41.858766805,2023-10-28 04:24:00,Fred Luddy,INC0000000391,system,United States,Howard Johnson,David Loo,2023-11-07 22:41:41.858766805,Network outage in server room,Network,2 - High
|
||||
Hardware,Resolved,2023-11-04 00:17:56.334785377,2023-10-29 03:27:00,Howard Johnson,INC0000000392,employee,Australia,Howard Johnson,David Loo,2023-11-04 00:17:56.334785377,Printer546 is malfunctioning,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-14 14:50:37.041296707,2023-10-29 03:41:00,Howard Johnson,INC0000000393,employee,Canada,Howard Johnson,Bud Richman,2023-11-14 14:50:37.041296707,PC453 failure,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-11-11 05:32:19.072152631,2023-10-29 14:02:00,Beth Anglin,INC0000000394,employee,Australia,Luke Wilson,ITIL User,2023-11-11 05:32:19.072152631,Printer546 is not working properly in Australia,Hardware,1 - Critical
|
||||
Inquiry / Help,Resolved,2023-11-01 13:29:20.811811166,2023-10-29 17:52:00,Beth Anglin,INC0000000395,employee,UK,Beth Anglin,David Loo,2023-11-01 13:29:20.811811166,Need help with navigating the software,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-10-30 18:57:00.000000000,2023-10-29 18:57:00,Howard Johnson,INC0000000396,employee,United States,Fred Luddy,ITIL User,2023-10-30 18:57:00.000000000,The Desktop PC7302 doesn't power on,Hardware,1 - Critical
|
||||
Database,Resolved,2023-11-12 08:10:38.248010764,2023-10-31 23:16:00,Luke Wilson,INC0000000397,system,United States,Beth Anglin,Don Goodliffe,2023-11-12 08:10:38.248010764,SQL Error on Server DB4,Database,2 - High
|
||||
Hardware,Resolved,2023-11-03 11:52:00.000000000,2023-11-02 11:52:00,Howard Johnson,INC0000000398,employee,Australia,Beth Anglin,David Loo,2023-11-03 11:52:00.000000000,Printer546 is not working,Hardware,1 - Critical
|
||||
Inquiry / Help,Resolved,2023-11-22 14:54:17.505991208,2023-11-02 12:42:00,Luke Wilson,INC0000000399,system,UK,Charlie Whitherspoon,Don Goodliffe,2023-11-22 14:54:17.505991208,Need assistance with account settings,Service Desk,3 - Moderate
|
||||
Hardware,Resolved,2023-11-19 08:35:26.721892000,2023-11-03 09:56:00,Fred Luddy,INC0000000400,system,Australia,Beth Anglin,ITIL User,2023-11-19 08:35:26.721892000,Printer542 in Australia is malfunctioning,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-07 04:08:46.640926620,2023-11-03 13:49:00,Charlie Whitherspoon,INC0000000401,system,Australia,Fred Luddy,Don Goodliffe,2023-11-07 04:08:46.640926620,Printer issue with Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-11-06 18:24:09.637921817,2023-11-04 10:59:00,Charlie Whitherspoon,INC0000000402,employee,Australia,Beth Anglin,Don Goodliffe,2023-11-06 18:24:09.637921817,Printer546 is not responding,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-20 14:15:29.239228781,2023-11-05 06:44:00,Charlie Whitherspoon,INC0000000403,employee,India,Howard Johnson,ITIL User,2023-11-20 14:15:29.239228781,Server crash in Bangalore,Hardware,1 - Critical
|
||||
Database,Resolved,2023-11-15 23:12:53.763111504,2023-11-05 11:29:00,Charlie Whitherspoon,INC0000000404,employee,Australia,Howard Johnson,Don Goodliffe,2023-11-15 23:12:53.763111504,Unexpected shutdown of Database439 in Australia,Database,2 - High
|
||||
Hardware,Closed,2023-11-08 06:15:25.825398065,2023-11-05 19:34:00,Charlie Whitherspoon,INC0000000405,employee,Australia,Fred Luddy,Bud Richman,2023-11-08 06:15:25.825398065,Printer167 has a paper jam,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-11-12 08:41:55.212270110,2023-11-06 04:39:00,Charlie Whitherspoon,INC0000000406,employee,Australia,Beth Anglin,ITIL User,2023-11-12 08:41:55.212270110,Printer546 is not working,Hardware,1 - Critical
|
||||
Inquiry / Help,Closed,2023-11-07 11:30:00.000000000,2023-11-06 11:30:00,Fred Luddy,INC0000000407,admin,UK,Howard Johnson,ITIL User,2023-11-07 11:30:00.000000000,Need help with software installation,Service Desk,3 - Moderate
|
||||
Database,Closed,2023-11-11 22:19:34.292012026,2023-11-07 02:57:00,Beth Anglin,INC0000000408,admin,Canada,Beth Anglin,Bud Richman,2023-11-11 22:19:34.292012026,Database error on DB546 server,Database,2 - High
|
||||
Software,Resolved,2023-11-13 13:05:35.772593402,2023-11-07 19:50:00,Beth Anglin,INC0000000409,employee,UK,Charlie Whitherspoon,Bud Richman,2023-11-13 13:05:35.772593402,Software crash on startup,Software,2 - High
|
||||
Hardware,Closed,2023-11-18 17:10:46.429479768,2023-11-08 05:51:00,Howard Johnson,INC0000000410,employee,UK,Charlie Whitherspoon,ITIL User,2023-11-18 17:10:46.429479768,Faulty keyboard on desktop model XJ56,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-24 06:22:46.772217434,2023-11-08 19:58:00,Beth Anglin,INC0000000411,employee,United States,Luke Wilson,Don Goodliffe,2023-11-24 06:22:46.772217434,Faulty motherboard on desktop PC,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-14 12:04:58.180425139,2023-11-09 00:45:00,Howard Johnson,INC0000000412,employee,Australia,Howard Johnson,ITIL User,2023-11-14 12:04:58.180425139,Printer320 is not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-19 06:43:16.486790150,2023-11-09 12:54:00,Fred Luddy,INC0000000413,system,Australia,Charlie Whitherspoon,Bud Richman,2023-11-19 06:43:16.486790150,Printer456 in Australia is not printing,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-20 13:58:24.005162219,2023-11-10 16:18:00,Fred Luddy,INC0000000414,employee,Australia,Fred Luddy,Bud Richman,2023-11-20 13:58:24.005162219,Printer789 is not functioning properly,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-25 13:45:38.979607981,2023-11-11 21:57:00,Charlie Whitherspoon,INC0000000415,admin,India,Howard Johnson,Don Goodliffe,2023-11-25 13:45:38.979607981,Problem with desktop system - overloaded memory,Hardware,2 - High
|
||||
Database,Closed,2023-11-23 08:35:34.467772083,2023-11-12 04:33:00,Charlie Whitherspoon,INC0000000416,admin,United States,Howard Johnson,Bud Richman,2023-11-23 08:35:34.467772083,Database error in production environment,Database,2 - High
|
||||
Database,Resolved,2023-11-15 18:59:00.000000000,2023-11-14 18:59:00,Charlie Whitherspoon,INC0000000417,system,India,Beth Anglin,David Loo,2023-11-15 18:59:00.000000000,Oracle Database error reported,Database,2 - High
|
||||
Hardware,Resolved,2023-11-27 06:06:19.635851412,2023-11-14 19:32:00,Fred Luddy,INC0000000418,employee,UK,Beth Anglin,Bud Richman,2023-11-27 06:06:19.635851412,Replacing faulty components in the system,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-23 15:56:17.027339265,2023-11-15 07:35:00,Charlie Whitherspoon,INC0000000419,employee,Australia,Beth Anglin,ITIL User,2023-11-23 15:56:17.027339265,Printer546 is not functioning,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-28 08:39:37.872085078,2023-11-15 19:05:00,Howard Johnson,INC0000000420,employee,Australia,Howard Johnson,Bud Richman,2023-11-28 08:39:37.872085078,Printer546 is not working properly,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-21 01:30:21.296528133,2023-11-17 07:33:00,Charlie Whitherspoon,INC0000000421,system,United States,Charlie Whitherspoon,Don Goodliffe,2023-11-21 01:30:21.296528133,Hard drive failure on desktop PRCS4526,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-23 05:26:06.568026419,2023-11-17 11:41:00,Luke Wilson,INC0000000422,system,UK,Beth Anglin,Bud Richman,2023-11-23 05:26:06.568026419,Failure in Screen Resolution - System ID: 7843,Hardware,2 - High
|
||||
Hardware,Closed,2023-11-19 13:31:44.558204717,2023-11-18 02:05:00,Howard Johnson,INC0000000423,admin,Canada,Luke Wilson,ITIL User,2023-11-19 13:31:44.558204717,Failure reported at desktop232,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-11-25 18:11:25.410173561,2023-11-19 02:52:00,Luke Wilson,INC0000000424,employee,Australia,Charlie Whitherspoon,Bud Richman,2023-11-25 18:11:25.410173561,Printer123 issue in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-11-24 19:39:37.248766516,2023-11-19 11:06:00,Charlie Whitherspoon,INC0000000425,admin,Australia,Beth Anglin,Don Goodliffe,2023-11-24 19:39:37.248766516,Printer463 is not working,Hardware,2 - High
|
||||
Software,Resolved,2023-11-27 12:48:59.344411018,2023-11-21 04:00:00,Luke Wilson,INC0000000426,admin,UK,Charlie Whitherspoon,Bud Richman,2023-11-27 12:48:59.344411018,"Software malfunctioning, system crash",Software,3 - Moderate
|
||||
Hardware,Resolved,2023-11-29 21:52:24.416389196,2023-11-22 08:43:00,Beth Anglin,INC0000000427,admin,Australia,Luke Wilson,ITIL User,2023-11-29 21:52:24.416389196,Printer786 is not responding,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-11-29 19:40:37.040547419,2023-11-23 08:47:00,Luke Wilson,INC0000000428,admin,Australia,Howard Johnson,ITIL User,2023-11-29 19:40:37.040547419,Issue with Printer546 in Australia,Hardware,2 - High
|
||||
Database,Closed,2023-12-01 23:08:08.172813661,2023-11-23 15:24:00,Beth Anglin,INC0000000429,employee,Australia,Luke Wilson,David Loo,2023-12-01 23:08:08.172813661,Facing issues with MySQL database connectivity,Database,2 - High
|
||||
Hardware,Resolved,2023-12-09 22:53:40.047263709,2023-11-24 19:27:00,Luke Wilson,INC0000000430,admin,Australia,Charlie Whitherspoon,Bud Richman,2023-12-09 22:53:40.047263709,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Inquiry / Help,Resolved,2023-12-06 22:33:01.700205220,2023-11-27 20:26:00,Beth Anglin,INC0000000431,employee,Australia,Fred Luddy,David Loo,2023-12-06 22:33:01.700205220,Printer 546 is malfunctioning,Hardware,3 - Moderate
|
||||
Hardware,Resolved,2023-12-13 03:48:19.521259680,2023-11-28 06:30:00,Fred Luddy,INC0000000432,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2023-12-13 03:48:19.521259680,Printer546 is malfunctioning in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-06 06:18:20.231641812,2023-11-29 01:39:00,Luke Wilson,INC0000000433,employee,Australia,Beth Anglin,Bud Richman,2023-12-06 06:18:20.231641812,Printer422 is continuously jamming,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-05 17:32:49.113615094,2023-11-29 07:33:00,Beth Anglin,INC0000000434,system,Australia,Charlie Whitherspoon,Bud Richman,2023-12-05 17:32:49.113615094,Printer546 malfunction in Australia,Hardware,1 - Critical
|
||||
Hardware,Closed,2023-12-01 04:31:00.000000000,2023-11-30 04:31:00,Charlie Whitherspoon,INC0000000435,system,Australia,Fred Luddy,Bud Richman,2023-12-01 04:31:00.000000000,Printer546 is malfunctioning,Hardware,1 - Critical
|
||||
Network,Resolved,2023-12-05 02:10:03.357753705,2023-12-01 04:34:00,Charlie Whitherspoon,INC0000000436,system,Canada,Luke Wilson,Bud Richman,2023-12-05 02:10:03.357753705,Router not working properly,Network,2 - High
|
||||
Network,Closed,2023-12-10 05:44:39.457781801,2023-12-02 09:51:00,Howard Johnson,INC0000000437,system,United States,Charlie Whitherspoon,Bud Richman,2023-12-10 05:44:39.457781801,Can't connect to VPN,Network,2 - High
|
||||
Hardware,Closed,2023-12-09 17:15:01.861232926,2023-12-03 07:21:00,Howard Johnson,INC0000000438,admin,Australia,Luke Wilson,David Loo,2023-12-09 17:15:01.861232926,Printer issue with Printer546,Hardware,2 - High
|
||||
Hardware,Resolved,2023-12-07 19:08:00.907490315,2023-12-04 13:22:00,Charlie Whitherspoon,INC0000000439,admin,Australia,Beth Anglin,David Loo,2023-12-07 19:08:00.907490315,Printer547 has a paper jam,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-06 21:57:38.764148014,2023-12-04 20:14:00,Luke Wilson,INC0000000440,system,Australia,Charlie Whitherspoon,Don Goodliffe,2023-12-06 21:57:38.764148014,Printer678 not working,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-15 17:24:42.185263024,2023-12-04 20:19:00,Fred Luddy,INC0000000441,employee,Australia,Charlie Whitherspoon,David Loo,2023-12-15 17:24:42.185263024,Printer542 is not printing,Hardware,2 - High
|
||||
Hardware,Resolved,2023-12-09 12:22:44.658032345,2023-12-05 11:43:00,Charlie Whitherspoon,INC0000000442,system,Australia,Beth Anglin,Don Goodliffe,2023-12-09 12:22:44.658032345,Printer546 is not working,Hardware,1 - Critical
|
||||
Network,Resolved,2023-12-14 06:24:13.933877695,2023-12-05 15:02:00,Fred Luddy,INC0000000443,admin,UK,Beth Anglin,ITIL User,2023-12-14 06:24:13.933877695,Unable to connect to VPN Server,Network,2 - High
|
||||
Inquiry / Help,Resolved,2023-12-07 16:11:16.045197690,2023-12-06 00:31:00,Fred Luddy,INC0000000444,system,United States,Howard Johnson,ITIL User,2023-12-07 16:11:16.045197690,Need assistance with Service Desk application,Service Desk,3 - Moderate
|
||||
Hardware,Closed,2023-12-17 00:37:06.937192986,2023-12-07 00:44:00,Howard Johnson,INC0000000445,employee,Australia,Howard Johnson,Bud Richman,2023-12-17 00:37:06.937192986,Printer546 is not printing in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2023-12-19 02:53:45.256687900,2023-12-08 00:32:00,Howard Johnson,INC0000000446,system,Australia,Howard Johnson,ITIL User,2023-12-19 02:53:45.256687900,Printer error on Printer546,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-15 13:12:56.653826436,2023-12-09 03:58:00,Luke Wilson,INC0000000447,system,Australia,Charlie Whitherspoon,Bud Richman,2023-12-15 13:12:56.653826436,Printer546 is not working,Hardware,3 - Moderate
|
||||
Hardware,Closed,2023-12-11 12:52:00.000000000,2023-12-10 12:52:00,Howard Johnson,INC0000000448,employee,United States,Howard Johnson,David Loo,2023-12-11 12:52:00.000000000,Server HW897 malfunction,Hardware,2 - High
|
||||
Hardware,Resolved,2023-12-14 20:37:19.388113353,2023-12-11 17:57:00,Charlie Whitherspoon,INC0000000449,employee,Canada,Luke Wilson,Bud Richman,2023-12-14 20:37:19.388113353,Issue with workstation PC554 in Canada,Hardware,3 - Moderate
|
||||
Database,Closed,2023-12-12 19:10:00.000000000,2023-12-11 19:10:00,Beth Anglin,INC0000000450,system,UK,Luke Wilson,Bud Richman,2023-12-12 19:10:00.000000000,UK Database server 307 outage,Database,1 - Critical
|
||||
Software,Resolved,2023-12-14 13:02:12.152455924,2023-12-12 08:51:00,Howard Johnson,INC0000000451,system,UK,Beth Anglin,ITIL User,2023-12-14 13:02:12.152455924,Unable to install updates,Software,2 - High
|
||||
Hardware,Resolved,2023-12-17 23:44:55.013288975,2023-12-12 15:28:00,Beth Anglin,INC0000000452,employee,Australia,Fred Luddy,ITIL User,2023-12-17 23:44:55.013288975,Printer error with Printer546 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-12-15 03:54:00.000000000,2023-12-14 03:54:00,Beth Anglin,INC0000000453,admin,Australia,Fred Luddy,David Loo,2023-12-15 03:54:00.000000000,Printer356 is not functioning properly,Hardware,1 - Critical
|
||||
Network,Closed,2023-12-22 14:45:04.366600219,2023-12-17 08:06:00,Fred Luddy,INC0000000454,system,India,Howard Johnson,Bud Richman,2023-12-22 14:45:04.366600219,Network connectivity issue in India,Network,2 - High
|
||||
Database,Closed,2024-01-04 00:08:18.653166818,2023-12-18 04:38:00,Fred Luddy,INC0000000455,system,Australia,Fred Luddy,Bud Richman,2024-01-04 00:08:18.653166818,Database error in patient records system,Database,2 - High
|
||||
Hardware,Closed,2023-12-26 08:26:25.509637696,2023-12-18 11:13:00,Luke Wilson,INC0000000456,system,Australia,Howard Johnson,ITIL User,2023-12-26 08:26:25.509637696,Printer546 is not working properly in Australia,Hardware,2 - High
|
||||
Hardware,Closed,2023-12-28 23:19:22.500984917,2023-12-19 05:43:00,Howard Johnson,INC0000000457,admin,Australia,Charlie Whitherspoon,David Loo,2023-12-28 23:19:22.500984917,Printer546 is not working,Hardware,1 - Critical
|
||||
Hardware,Resolved,2024-01-01 04:47:56.741681309,2023-12-19 07:32:00,Luke Wilson,INC0000000458,admin,Australia,Luke Wilson,David Loo,2024-01-01 04:47:56.741681309,Issue with Printer487 in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2023-12-27 23:15:06.590296403,2023-12-23 18:43:00,Luke Wilson,INC0000000459,admin,Canada,Beth Anglin,ITIL User,2023-12-27 23:15:06.590296403,Laptop not functioning properly in Canada,Hardware,3 - Moderate
|
||||
Database,Resolved,2024-01-03 16:58:25.664592126,2023-12-26 00:19:00,Luke Wilson,INC0000000460,admin,India,Howard Johnson,ITIL User,2024-01-03 16:58:25.664592126,SQL server issue detected,Database,2 - High
|
||||
Network,Resolved,2024-01-09 12:37:13.292569926,2023-12-26 00:25:00,Beth Anglin,INC0000000461,employee,UK,Howard Johnson,David Loo,2024-01-09 12:37:13.292569926,UK Network server is down,Network,1 - Critical
|
||||
Hardware,Closed,2024-01-04 10:16:12.278901784,2023-12-26 16:08:00,Fred Luddy,INC0000000462,employee,Australia,Fred Luddy,David Loo,2024-01-04 10:16:12.278901784,Printer malfunction for Printer546 in Australia,Hardware,1 - Critical
|
||||
Hardware,Closed,2024-01-06 13:12:25.109624389,2023-12-28 06:15:00,Fred Luddy,INC0000000463,system,United States,Luke Wilson,Don Goodliffe,2024-01-06 13:12:25.109624389,Server789 crashed unexpectedly,Hardware,1 - Critical
|
||||
Hardware,Resolved,2024-01-03 16:45:19.710278908,2023-12-28 22:22:00,Fred Luddy,INC0000000464,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2024-01-03 16:45:19.710278908,Printer957 is malfunctioning in Australia.,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-06 22:55:13.977268119,2023-12-31 00:23:00,Luke Wilson,INC0000000465,employee,India,Fred Luddy,Don Goodliffe,2024-01-06 22:55:13.977268119,Desktop123 does not start,Hardware,2 - High
|
||||
Network,Resolved,2024-01-08 21:09:04.022142416,2024-01-02 05:33:00,Charlie Whitherspoon,INC0000000466,admin,Canada,Luke Wilson,David Loo,2024-01-08 21:09:04.022142416,Network connectivity issue,Network,3 - Moderate
|
||||
Hardware,Resolved,2024-01-08 09:17:43.178873486,2024-01-02 23:29:00,Howard Johnson,INC0000000467,employee,Australia,Fred Luddy,Bud Richman,2024-01-08 09:17:43.178873486,Issue with Printer546 in Australia,Hardware,3 - Moderate
|
||||
Hardware,Closed,2024-01-16 19:13:13.303553162,2024-01-04 15:03:00,Howard Johnson,INC0000000468,admin,United States,Beth Anglin,Don Goodliffe,2024-01-16 19:13:13.303553162,Computer station 420 is not functioning,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-10 15:38:48.582320090,2024-01-05 03:27:00,Beth Anglin,INC0000000469,admin,India,Fred Luddy,Don Goodliffe,2024-01-10 15:38:48.582320090,Failed motherboard on HP PC,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-07 03:03:46.511421975,2024-01-05 10:47:00,Beth Anglin,INC0000000470,admin,Australia,Howard Johnson,Bud Richman,2024-01-07 03:03:46.511421975,Printer787 in Australia location is not working,Hardware,2 - High
|
||||
Network,Closed,2024-01-14 00:18:47.213476730,2024-01-05 16:43:00,Howard Johnson,INC0000000471,employee,India,Luke Wilson,Bud Richman,2024-01-14 00:18:47.213476730,Slow network connection in Office 5,Network,3 - Moderate
|
||||
Hardware,Closed,2024-01-20 12:12:40.568716377,2024-01-07 21:25:00,Charlie Whitherspoon,INC0000000472,employee,Canada,Charlie Whitherspoon,David Loo,2024-01-20 12:12:40.568716377,Desktop PC not starting up,Hardware,3 - Moderate
|
||||
Hardware,Closed,2024-01-13 07:11:07.585466982,2024-01-09 07:10:00,Luke Wilson,INC0000000473,system,UK,Charlie Whitherspoon,Bud Richman,2024-01-13 07:11:07.585466982,The laptop hard drive is failing,Hardware,2 - High
|
||||
Software,Closed,2024-01-19 07:24:42.210493722,2024-01-10 15:21:00,Beth Anglin,INC0000000474,system,Australia,Howard Johnson,Bud Richman,2024-01-19 07:24:42.210493722,Application error in windows 10,Software,2 - High
|
||||
Hardware,Closed,2024-01-14 19:07:03.245823840,2024-01-11 05:07:00,Luke Wilson,INC0000000475,employee,Canada,Charlie Whitherspoon,David Loo,2024-01-14 19:07:03.245823840,Issue with motherboard on desktop PC,Hardware,2 - High
|
||||
Hardware,Closed,2024-01-21 14:31:46.018925216,2024-01-13 06:18:00,Charlie Whitherspoon,INC0000000476,employee,Australia,Charlie Whitherspoon,Don Goodliffe,2024-01-21 14:31:46.018925216,Printer546 is malfunctioning,Hardware,1 - Critical
|
||||
Hardware,Resolved,2024-01-19 04:34:40.251168087,2024-01-15 02:05:00,Fred Luddy,INC0000000477,system,Australia,Howard Johnson,Bud Richman,2024-01-19 04:34:40.251168087,Printer546 is malfunctioning in Australia,Hardware,1 - Critical
|
||||
Hardware,Resolved,2024-01-21 17:52:21.410495977,2024-01-15 14:42:00,Luke Wilson,INC0000000478,admin,Australia,Beth Anglin,ITIL User,2024-01-21 17:52:21.410495977,Printer128 is not working properly in Australia,Hardware,3 - Moderate
|
||||
Software,Resolved,2024-01-23 07:05:32.181816124,2024-01-15 16:26:00,Beth Anglin,INC0000000479,system,United States,Beth Anglin,Don Goodliffe,2024-01-23 07:05:32.181816124,Unable to install an update on Office365,Software,2 - High
|
||||
Database,Resolved,2024-01-26 14:08:14.089327678,2024-01-16 10:00:00,Howard Johnson,INC0000000480,employee,Australia,Beth Anglin,Bud Richman,2024-01-26 14:08:14.089327678,Oracle Database 11g - Error Ora-02000,Database,2 - High
|
||||
Software,Closed,2024-01-19 09:54:00.000000000,2024-01-18 09:54:00,Howard Johnson,INC0000000481,admin,United States,Fred Luddy,David Loo,2024-01-19 09:54:00.000000000,Software update required,Software,2 - High
|
||||
Database,Resolved,2024-01-24 20:11:01.690566157,2024-01-19 21:43:00,Howard Johnson,INC0000000482,system,Australia,Beth Anglin,David Loo,2024-01-24 20:11:01.690566157,Issue with Database543,Database,3 - Moderate
|
||||
Hardware,Resolved,2024-01-21 13:40:00.000000000,2024-01-20 13:40:00,Luke Wilson,INC0000000483,system,Australia,Luke Wilson,Don Goodliffe,2024-01-21 13:40:00.000000000,Printer542 is jammed in Australia location,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-22 06:01:00.000000000,2024-01-21 06:01:00,Luke Wilson,INC0000000484,system,Australia,Beth Anglin,ITIL User,2024-01-22 06:01:00.000000000,Printer546 is not working properly in Australia location,Hardware,1 - Critical
|
||||
Hardware,Resolved,2024-02-01 06:07:10.554650244,2024-01-21 09:04:00,Howard Johnson,INC0000000485,system,Australia,Luke Wilson,Don Goodliffe,2024-02-01 06:07:10.554650244,Printer546 is not working properly,Hardware,3 - Moderate
|
||||
Software,Closed,2024-01-27 16:50:43.924769422,2024-01-21 19:11:00,Luke Wilson,INC0000000486,employee,Australia,Beth Anglin,David Loo,2024-01-27 16:50:43.924769422,Application update failure,Software,2 - High
|
||||
Hardware,Closed,2024-01-24 00:12:58.375121721,2024-01-22 14:06:00,Fred Luddy,INC0000000487,employee,Australia,Luke Wilson,Don Goodliffe,2024-01-24 00:12:58.375121721,Printer436 is broken,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-29 03:16:08.014433635,2024-01-23 03:20:00,Beth Anglin,INC0000000488,system,Australia,Howard Johnson,Bud Richman,2024-01-29 03:16:08.014433635,Printer546 issue in Australia,Hardware,2 - High
|
||||
Hardware,Resolved,2024-01-25 11:01:00.578026325,2024-01-24 06:55:00,Charlie Whitherspoon,INC0000000489,admin,Australia,Charlie Whitherspoon,Don Goodliffe,2024-01-25 11:01:00.578026325,Printer627 is not responding,Hardware,1 - Critical
|
||||
Inquiry / Help,Resolved,2024-02-09 04:30:29.586553971,2024-01-24 16:35:00,Luke Wilson,INC0000000490,admin,India,Luke Wilson,David Loo,2024-02-09 04:30:29.586553971,Need help with inquiry,Service Desk,3 - Moderate
|
||||
Software,Closed,2024-02-01 02:14:30.096751951,2024-01-25 11:19:00,Beth Anglin,INC0000000491,system,UK,Howard Johnson,Don Goodliffe,2024-02-01 02:14:30.096751951,Application error on Accounting software,Software,2 - High
|
||||
Inquiry / Help,Resolved,2024-01-31 02:23:56.445975993,2024-01-25 18:57:00,Beth Anglin,INC0000000492,admin,Australia,Charlie Whitherspoon,David Loo,2024-01-31 02:23:56.445975993,Printer546 issue in Sydney office.,Hardware,2 - High
|
||||
Network,Closed,2024-02-01 12:40:26.195788814,2024-01-26 08:59:00,Fred Luddy,INC0000000493,admin,UK,Charlie Whitherspoon,ITIL User,2024-02-01 12:40:26.195788814,Unable to connect to the Wi-Fi,Network,2 - High
|
||||
Hardware,Resolved,2024-02-08 06:58:35.863529275,2024-01-27 11:50:00,Luke Wilson,INC0000000494,system,Australia,Howard Johnson,Bud Richman,2024-02-08 06:58:35.863529275,Printer489 is not functioning properly,Hardware,2 - High
|
||||
Network,Closed,2024-02-07 21:56:58.515415708,2024-01-28 15:54:00,Beth Anglin,INC0000000495,employee,India,Fred Luddy,David Loo,2024-02-07 21:56:58.515415708,Unable to connect to the internet,Network,2 - High
|
||||
Hardware,Resolved,2024-02-03 17:41:53.299049427,2024-01-29 10:48:00,Luke Wilson,INC0000000496,admin,Australia,Beth Anglin,David Loo,2024-02-03 17:41:53.299049427,Printer546 has a paper jam,Hardware,2 - High
|
||||
Network,Resolved,2024-02-12 22:31:48.126196708,2024-01-31 05:51:00,Howard Johnson,INC0000000497,admin,Canada,Charlie Whitherspoon,David Loo,2024-02-12 22:31:48.126196708,Internet connection frequently dropping,Network,2 - High
|
||||
Network,Resolved,2024-02-07 23:14:21.894263712,2024-01-31 18:48:00,Charlie Whitherspoon,INC0000000498,employee,Australia,Howard Johnson,David Loo,2024-02-07 23:14:21.894263712,Network outage in Australia,Network,2 - High
|
||||
Hardware,Closed,2024-02-11 16:34:09.159779461,2024-01-31 21:20:00,Beth Anglin,INC0000000499,employee,Australia,Fred Luddy,ITIL User,2024-02-11 16:34:09.159779461,Printer235 is not printing in Australia location,Hardware,1 - Critical
|
||||
|
BIN
alias/frontend/src/assets/icons/avatar/assistantHeader.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
1
alias/frontend/src/assets/icons/avatar/avatar.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><g><g><path d="M2,8C2,4.68629,4.68629,2,8,2C11.3137,2,14,4.68629,14,8C14,11.3137,11.3137,14,8,14C4.68629,14,2,11.3137,2,8ZM11.53555,4.46447C11.04738,3.97631,10.48235,3.60517,9.84045,3.35106C9.249279999999999,3.11702,8.6358,3,8,3C7.3642,3,6.75071,3.11702,6.15952,3.35106C5.51764,3.60517,4.95262,3.97631,4.46446,4.46447C3.97631,4.95262,3.60517,5.51764,3.35106,6.15953C3.11702,6.75071,3,7.3642,3,8C3,8.6358,3.11702,9.249279999999999,3.35106,9.84045C3.60517,10.48235,3.97631,11.04738,4.46446,11.53555C4.95262,12.0237,5.51764,12.3949,6.15952,12.649C6.75071,12.883,7.3642,13,8,13C8.6358,13,9.249279999999999,12.883,9.84045,12.649C10.48235,12.3948,11.04738,12.0237,11.53555,11.53555C12.0237,11.04738,12.3949,10.48235,12.649,9.84045C12.883,9.249279999999999,13,8.6358,13,8C13,7.3642,12.883,6.75071,12.649,6.15953C12.3948,5.51764,12.0237,4.95262,11.53555,4.46447ZM7.55356,9.95356C7.35829,10.14882,7.04171,10.14882,6.84645,9.95356L5.246449999999999,8.35356C5.15268,8.259789999999999,5.1,8.13261,5.1,8C5.1,7.72386,5.32386,7.5,5.6,7.5C5.73261,7.5,5.85979,7.55268,5.9535599999999995,7.64645L7.2,8.892900000000001L10.04645,6.04645C10.1402,5.95268,10.2674,5.9,10.4,5.9C10.67615,5.9,10.9,6.12386,10.9,6.4C10.9,6.53261,10.8473,6.65979,10.75355,6.75356L7.55356,9.95356Z" fill="#00B042" fill-opacity="1"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
4
alias/frontend/src/assets/icons/files/doc.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="8" fill="#4285F4"/> <!-- Blue background -->
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#FFFFFF" font-size="14" font-family="Arial, sans-serif">DOC</text> <!-- Display 'DOC' text in the center -->
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
12
alias/frontend/src/assets/icons/files/file.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Define gray-white gradient -->
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#B0B0B0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FFFFFF;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Use gradient background -->
|
||||
<rect width="40" height="40" rx="8" fill="url(#grad1)"/>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#555555" font-size="14" font-family="Arial, sans-serif">FILE</text> <!-- Use lighter black -->
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
4
alias/frontend/src/assets/icons/files/html.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="8" fill="#FFBB00"/> <!-- Use yellow background -->
|
||||
<text x="50%" y="50%" fill="white" font-size="12" font-family="Arial" font-weight="bold" text-anchor="middle" dominant-baseline="middle">HTML</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
12
alias/frontend/src/assets/icons/files/image.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Define blue-white gradient -->
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4285F4;stop-opacity:1" /> <!-- Blue start -->
|
||||
<stop offset="100%" style="stop-color:#FFFFFF;stop-opacity:1" /> <!-- White end -->
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Use blue-white gradient background -->
|
||||
<rect width="40" height="40" rx="8" fill="url(#grad1)"/>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#FFFFFF" font-size="14" font-family="Arial, sans-serif">IMG</text> <!-- Use white text -->
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 671 B |
4
alias/frontend/src/assets/icons/files/pdf.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="8" fill="#EA4335"/> <!-- Use red background -->
|
||||
<text x="50%" y="50%" fill="white" font-size="14" font-family="Arial" font-weight="bold" text-anchor="middle" dominant-baseline="middle">PDF</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 304 B |
4
alias/frontend/src/assets/icons/files/xml.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="8" fill="#34A853"/> <!-- Use green background -->
|
||||
<text x="50%" y="50%" fill="white" font-size="14" font-family="Arial" font-weight="bold" text-anchor="middle" dominant-baseline="middle">XML</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
1
alias/frontend/src/assets/icons/sharePage/circle401.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="9" height="9" viewBox="0 0 9 9"><g><ellipse cx="4.5" cy="4.5" rx="4.5" ry="4.5" fill="#000000" fill-opacity="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 239 B |
5
alias/frontend/src/assets/icons/sharePage/pause.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="6" y="5" width="4" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="14" y="5" width="4" height="14" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
4
alias/frontend/src/assets/icons/sharePage/play.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 5L19 12L8 19V5Z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
5
alias/frontend/src/assets/icons/sharePage/step-back.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 5L4 12L11 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 12H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 5L20 12L13 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 12H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
205
alias/frontend/src/assets/json/prompt.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
// Match the corresponding prompt key based on chatMode
|
||||
export const promptJson = {
|
||||
// General
|
||||
general: [
|
||||
{
|
||||
title: "Alibaba Stock Price",
|
||||
describe: "What's the current stock price of Alibaba?",
|
||||
},
|
||||
{
|
||||
title: "Travel Plan to Hangzhou",
|
||||
describe:
|
||||
"Help generate a two-day travel plan to Hangzhou this weekend from Shanghai",
|
||||
},
|
||||
{
|
||||
title: "Alibaba Stock Report",
|
||||
describe:
|
||||
"Generate a detailed report about Alibaba stock price in US market",
|
||||
},
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia’s financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
],
|
||||
// Browser Use
|
||||
browser: [
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia’s financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
],
|
||||
// Deep Research
|
||||
dr: [
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia’s financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
],
|
||||
// Financial Analysis
|
||||
finance: [
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia’s financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
],
|
||||
// Data Science The file needs to be placed in /src/assets/file
|
||||
// files: The files to the file to be uploaded. The files format is "/src/assets/file/filename" or ["/src/assets/file/filename"].
|
||||
ds: [
|
||||
{
|
||||
title: "Analysis of Disparities in Incident Allocation by Category",
|
||||
describe:
|
||||
"Find the discrepancy and imbalance in distribution of incidents assigned across categories",
|
||||
files: ["/src/assets/file/incident_records.csv"],
|
||||
},
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia’s financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// If chatMode doesn't match any above, use the following
|
||||
export const originalPromptsList = [
|
||||
{
|
||||
title: "Alibaba Stock Price",
|
||||
describe: "What's the current stock price of Alibaba?",
|
||||
},
|
||||
{
|
||||
title: "Travel Plan to Hangzhou",
|
||||
describe:
|
||||
"Help generate a two-day travel plan to Hangzhou this weekend from Shanghai",
|
||||
},
|
||||
{
|
||||
title: "Alibaba Stock Report",
|
||||
describe:
|
||||
"Generate a detailed report about Alibaba stock price in US market",
|
||||
},
|
||||
{
|
||||
title: "iPhone 17 vs 17 Pro",
|
||||
describe: "Give a detailed comparison between iPhone 17 and 17 Pro",
|
||||
},
|
||||
{
|
||||
title: "AI Agents Courses on Coursera",
|
||||
describe:
|
||||
"On Coursera, find 3 popular beginner-level courses about AI agents",
|
||||
},
|
||||
{
|
||||
title: "AI Service Investment",
|
||||
describe:
|
||||
"Evaluate the investment outlook and primary risks for AI infrastructure software and services outside the core AI chip sector over the next year, driven by large language models and edge computing",
|
||||
},
|
||||
{
|
||||
title: "Fed Rate Outlook",
|
||||
describe:
|
||||
"Conduct a deep analysis of the expected Federal Reserve (Fed) interest rate cuts in early 2026",
|
||||
},
|
||||
{
|
||||
title: "Nvidia Future Earnings",
|
||||
describe: "Forecast Nvidia's financial data for the coming year (2026)",
|
||||
},
|
||||
{
|
||||
title: "US-China Chip Impact",
|
||||
describe:
|
||||
"Perform a comprehensive analysis of the long-term impact of US-China trade tensions on the semiconductor supply chain.",
|
||||
},
|
||||
];
|
||||
20
alias/frontend/src/components/Artifacts/PanelHeader.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { memo } from "react";
|
||||
import { classNames } from "../../utils/classNames";
|
||||
|
||||
interface PanelHeaderProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center gap-2 bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor px-4 py-1 min-h-[34px] text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
49
alias/frontend/src/components/Artifacts/Tree.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
// Tree.scss
|
||||
$indent-base: 24px; // Base indent unit
|
||||
|
||||
.node {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
// padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
transition: padding 0.2s ease;
|
||||
|
||||
// Dynamically generate level styles (supports 1-8 levels)
|
||||
@for $i from 0 through 100 {
|
||||
&.level-#{$i} {
|
||||
padding-left: ($indent-base * $i) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--message-option-bg-hover);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--filenode-selected-bg);
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.node-text {
|
||||
font-family: system-ui;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
169
alias/frontend/src/components/Artifacts/Tree.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { NodeRendererProps } from "react-arborist";
|
||||
import { Tree } from "react-arborist";
|
||||
import {
|
||||
BsFile,
|
||||
BsFiles,
|
||||
BsFiletypeCss,
|
||||
BsFiletypeHtml,
|
||||
BsFiletypeJava,
|
||||
BsFiletypeJs,
|
||||
BsFiletypeJsx,
|
||||
BsFiletypeMd,
|
||||
BsFiletypePy,
|
||||
BsFiletypeTsx,
|
||||
BsFolder2Open,
|
||||
BsImage,
|
||||
} from "react-icons/bs";
|
||||
import { FaFileExcel, FaFilePowerpoint, FaFileWord } from "react-icons/fa";
|
||||
import { FiArchive, FiFilm, FiFolder } from "react-icons/fi";
|
||||
import "./Tree.scss";
|
||||
// Node data type
|
||||
interface FileNode {
|
||||
name: string;
|
||||
type: "directory" | "file";
|
||||
path: string;
|
||||
children?: FileNode[];
|
||||
}
|
||||
|
||||
interface FileTreeProps {
|
||||
data: FileNode[];
|
||||
onExpand: (node: FileNode) => void;
|
||||
show: string;
|
||||
}
|
||||
|
||||
// Add sorting function
|
||||
const sortFileTree = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes
|
||||
.sort((a, b) => {
|
||||
// First sort by type (directories first)
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "directory" ? -1 : 1;
|
||||
}
|
||||
// Then sort by name alphabetically
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((node) => ({
|
||||
...node,
|
||||
children: node.children ? sortFileTree(node.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
function getFileType(name: string, isFolder: boolean, isOpen: boolean) {
|
||||
const ext = name.split(".").pop()?.toLowerCase();
|
||||
const iconStyle = { color: "#6b7280" };
|
||||
if (isFolder) {
|
||||
return isOpen ? (
|
||||
<BsFolder2Open className="icon" />
|
||||
) : (
|
||||
<FiFolder className="icon" />
|
||||
);
|
||||
}
|
||||
if (!ext) {
|
||||
return <BsFile style={iconStyle} className="icon" />;
|
||||
}
|
||||
switch (ext) {
|
||||
case "js":
|
||||
return <BsFiletypeJs style={{ color: "#f1e05a" }} className="icon" />;
|
||||
case "py":
|
||||
return <BsFiletypePy style={iconStyle} className="icon" />;
|
||||
case "md":
|
||||
return <BsFiletypeMd style={iconStyle} className="icon" />;
|
||||
case "tsx":
|
||||
return <BsFiletypeTsx style={iconStyle} className="icon" />;
|
||||
case "jsx":
|
||||
return <BsFiletypeJsx style={{ color: "#f1e05a" }} className="icon" />;
|
||||
case "java":
|
||||
return <BsFiletypeJava style={iconStyle} className="icon" />;
|
||||
case "json":
|
||||
return <BsFiles style={{ color: "#f5de19" }} className="icon" />;
|
||||
case "css":
|
||||
return <BsFiletypeCss style={iconStyle} className="icon" />;
|
||||
case "html":
|
||||
return <BsFiletypeHtml style={iconStyle} className="icon" />;
|
||||
case "png":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return <BsImage style={iconStyle} className="icon" />;
|
||||
case "docx":
|
||||
case "doc":
|
||||
return <FaFileWord style={{ color: "#2b579a" }} className="icon" />;
|
||||
case "zip":
|
||||
case "tar.gz":
|
||||
case "rar":
|
||||
return <FiArchive style={iconStyle} className="icon" />;
|
||||
case "xlsx":
|
||||
case "xls":
|
||||
return <FaFileExcel style={{ color: "#217346" }} className="icon" />;
|
||||
case "pptx":
|
||||
case "ppt":
|
||||
return <FaFilePowerpoint style={{ color: "#d24726" }} className="icon" />;
|
||||
case "mp4":
|
||||
case "mov":
|
||||
case "avi":
|
||||
return <FiFilm style={iconStyle} className="icon" />;
|
||||
default:
|
||||
return <BsFile style={iconStyle} className="icon" />;
|
||||
}
|
||||
}
|
||||
|
||||
export const FileTree = ({ data, onExpand, show }: FileTreeProps) => {
|
||||
// Sort data
|
||||
const sortedData = sortFileTree(data);
|
||||
|
||||
// Custom node renderer
|
||||
const CustomNode = ({
|
||||
node,
|
||||
style,
|
||||
dragHandle,
|
||||
}: NodeRendererProps<FileNode>) => {
|
||||
const isFolder = node.data.type === "directory";
|
||||
const icon = getFileType(node.data.name, isFolder, node.isOpen);
|
||||
// Correct method to get node level
|
||||
const getNodeLevel = () => {
|
||||
let level = 0;
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
level++;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return level - 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dragHandle}
|
||||
// className={`node ${node.state.isSelected ? 'selected' : ''}` }
|
||||
className={`node
|
||||
${node.state.isSelected ? "selected" : ""}
|
||||
level-${getNodeLevel()}
|
||||
`}
|
||||
onClick={() => {
|
||||
node.isInternal && node.toggle();
|
||||
if (show === "noShow") {
|
||||
onExpand(node?.data);
|
||||
}
|
||||
}}
|
||||
aria-expanded={node.isOpen}
|
||||
>
|
||||
<div className="node-content">
|
||||
{icon}
|
||||
<span className="node-text">{node.data.name}</span>
|
||||
{/* {node.isInternal && (
|
||||
<span className="arrow">{node.isOpen ? '▼' : '▶'}</span>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Tree<FileNode>
|
||||
data={sortedData}
|
||||
idAccessor={(node) => node.path}
|
||||
openByDefault={false}
|
||||
indent={20}
|
||||
width={"100%"}
|
||||
>
|
||||
{CustomNode}
|
||||
</Tree>
|
||||
);
|
||||
};
|
||||
83
alias/frontend/src/components/Artifacts/index.module.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
.artifactsWrap {
|
||||
width: 100%;
|
||||
height: calc(100vh - 134px);;
|
||||
border-top: solid 1px var(--sps-color-border-secondary);
|
||||
// background-color: var(--artifacts-wrap-bg);
|
||||
// box-shadow: 0 0 rgb(0 0 0 / 0);
|
||||
// border-radius: 10px;
|
||||
}
|
||||
.codeWrap{
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.treeWrap{
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
.terminalWrapper {
|
||||
height: 200px;
|
||||
min-height: 150px;
|
||||
max-height: 30vh;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background-color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminalHeader {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
background-color: var(--sidebar-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminalContainer {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
.modalTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
}
|
||||
.saveStatus {
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&.modified {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
&.saved {
|
||||
color: #155724;
|
||||
animation: fadeOut 3s ease-in-out forwards;
|
||||
}
|
||||
}
|
||||
.modalContent {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
70% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
105
alias/frontend/src/components/Artifacts/index.scss
Normal file
@@ -0,0 +1,105 @@
|
||||
.terminal {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
.terminal .xterm {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
// box-sizing: border-box;
|
||||
// min-width: 200px;
|
||||
// max-width: 680px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.fileName {
|
||||
line-height: 32px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iframe-box {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Add dark theme support
|
||||
.markdown-body.markdown-dark {
|
||||
color: #e0e0e0;
|
||||
background-color: #1a1a1a;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4da6ff;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #444444;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #b0b0b0;
|
||||
border-left-color: #444444;
|
||||
}
|
||||
|
||||
table {
|
||||
th {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-color: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: #444444;
|
||||
}
|
||||
}
|
||||
567
alias/frontend/src/components/Artifacts/index.tsx
Normal file
@@ -0,0 +1,567 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { Button, Modal, Tooltip } from "@agentscope-ai/design";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { java } from "@codemirror/lang-java";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { Prec } from "@codemirror/state";
|
||||
import { keymap } from "@codemirror/view";
|
||||
import { materialDark, materialLight } from "@uiw/codemirror-theme-material";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal as XTerm } from "@xterm/xterm";
|
||||
import { createTwoFilesPatch } from "diff";
|
||||
import "github-markdown-css/github-markdown-light.css";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import "katex/dist/katex.min.css";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import io, { Socket } from "socket.io-client";
|
||||
import "xterm/css/xterm.css";
|
||||
import { classNames } from "../../utils/classNames";
|
||||
import styles from "./index.module.scss";
|
||||
import "./index.scss";
|
||||
import { PanelHeader } from "./PanelHeader";
|
||||
import { FileTree } from "./Tree";
|
||||
const DEFAULT_TERMINAL_SIZE = 25;
|
||||
const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
|
||||
// const { DirectoryTree } = Tree;
|
||||
let socket: Socket;
|
||||
const darkTheme = {
|
||||
background: "#131414",
|
||||
foreground: "#ffffff",
|
||||
cursor: "#dddddd",
|
||||
};
|
||||
const lightTheme = {
|
||||
background: "#ffffff",
|
||||
foreground: "#333333",
|
||||
cursor: "#333333",
|
||||
};
|
||||
|
||||
const Artifacts = (Props: {
|
||||
webSocketUrl: { artifactsSio: any };
|
||||
runtimeToken: string;
|
||||
}) => {
|
||||
const [fileTree, setFileTree] = useState([]);
|
||||
const [codeValue, setCodeValue] = useState("");
|
||||
const [extensions, setExtensions] = useState([javascript()]);
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [markdownValue, setMarkdownValue] = useState("");
|
||||
const [htmlModal, setHtmlModal] = useState("");
|
||||
const [modal, setModal] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<"saved" | "modified" | null>(
|
||||
null,
|
||||
);
|
||||
const [originalContent, setOriginalContent] = useState("");
|
||||
const terminalElementRef = useRef<HTMLDivElement>(null);
|
||||
const artifactsSio = Props?.webSocketUrl?.artifactsSio;
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const runtimeToken = Props?.runtimeToken;
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const terminalRef = useRef<XTerm | null>(null);
|
||||
|
||||
const [lastCommand, setLastCommand] = useState("");
|
||||
const [isCommandRunning, setIsCommandRunning] = useState(false);
|
||||
const [commandOutput, setCommandOutput] = useState("");
|
||||
const lastCommandRef = useRef("");
|
||||
const { theme } = useTheme();
|
||||
|
||||
// Move handleOutput function to component top
|
||||
const handleOutput = (data: string) => {
|
||||
if (terminalElementRef.current) {
|
||||
const terminal = terminalElementRef.current.querySelector(
|
||||
".xterm",
|
||||
) as any;
|
||||
if (terminal && terminal.terminal) {
|
||||
terminal.terminal.write(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMessage = () => {
|
||||
let data: never[] = [];
|
||||
try {
|
||||
socket = io(artifactsSio, {
|
||||
path: "/artifacts/socket.io",
|
||||
auth: {
|
||||
token: runtimeToken,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to WebSocket:", error);
|
||||
// Handle connection failures, such as displaying an error message to the user
|
||||
}
|
||||
// socket = io("http://localhost:4500/artifacts");
|
||||
socket.on("connect", () => {
|
||||
console.log("Connection successful");
|
||||
// Reset all state
|
||||
setFileTree([]);
|
||||
setCodeValue("");
|
||||
setFileName("");
|
||||
setMarkdownValue("");
|
||||
setHtmlModal("");
|
||||
setModal(false);
|
||||
});
|
||||
socket.on("error", (event) => {
|
||||
// Error exception message
|
||||
console.log("error", event);
|
||||
});
|
||||
socket.emit("requestFileList");
|
||||
socket.on("fileTree", (fileTree) => {
|
||||
setFileTree(fileTree || []);
|
||||
data = fileTree;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
useEffect(() => {
|
||||
getMessage();
|
||||
|
||||
// Add timer to refresh file tree every 3 seconds
|
||||
const timer = setInterval(() => {
|
||||
if (socket) {
|
||||
socket.emit("requestFileList");
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
if (socket) {
|
||||
socket.off("connect");
|
||||
socket.off("error");
|
||||
socket.off("fileTree");
|
||||
socket.off("fileContent");
|
||||
socket.off("output", handleOutput);
|
||||
}
|
||||
};
|
||||
}, [artifactsSio]);
|
||||
const onExpand = (info: { type: string; path: any; name: string }) => {
|
||||
if (info?.type !== "file") {
|
||||
return;
|
||||
}
|
||||
const filePath = info?.path;
|
||||
socket.emit("loadFile", filePath);
|
||||
const handleFileContent = ({
|
||||
content,
|
||||
path,
|
||||
}: {
|
||||
content: string;
|
||||
path: string;
|
||||
}) => {
|
||||
setFileName(path);
|
||||
detectLanguage(path);
|
||||
setCodeValue(content);
|
||||
setOriginalContent(content);
|
||||
// Clear modification status when switching files
|
||||
setSaveStatus(null);
|
||||
if (path.split(".").slice(-1)[0] === "md") {
|
||||
setMarkdownValue(content);
|
||||
}
|
||||
if (path.split(".").slice(-1)[0] === "html") {
|
||||
setHtmlModal(content);
|
||||
}
|
||||
};
|
||||
socket.on("fileContent", handleFileContent);
|
||||
|
||||
return () => {
|
||||
socket.off("fileContent", handleFileContent);
|
||||
};
|
||||
};
|
||||
|
||||
const langMap = (fileExtension: string | undefined) => {
|
||||
let mode = javascript();
|
||||
switch (fileExtension) {
|
||||
case "css":
|
||||
mode = css();
|
||||
break;
|
||||
case "java":
|
||||
mode = java();
|
||||
break;
|
||||
case "ts":
|
||||
mode = javascript();
|
||||
break;
|
||||
case "js":
|
||||
mode = javascript();
|
||||
break;
|
||||
case "html":
|
||||
mode = html();
|
||||
break;
|
||||
case "py":
|
||||
mode = python();
|
||||
break;
|
||||
case "md":
|
||||
mode = markdown();
|
||||
break;
|
||||
}
|
||||
return mode;
|
||||
};
|
||||
// Language switching
|
||||
|
||||
const detectLanguage = (fileName: string) => {
|
||||
const ext = fileName.split(".").slice(-1)[0];
|
||||
const langLoader = langMap(ext);
|
||||
setExtensions([langLoader]);
|
||||
};
|
||||
|
||||
// Save file
|
||||
const saveKeymap = Prec.high(
|
||||
// Ensure shortcut key priority
|
||||
keymap.of([
|
||||
{
|
||||
key: "Ctrl-s",
|
||||
mac: "Cmd-s",
|
||||
run: (editor) => {
|
||||
saveCurrentFile(fileName);
|
||||
return true; // Prevent browser default behavior
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
const handleCodeChange = (value: string) => {
|
||||
setCodeValue(value);
|
||||
setSaveStatus("modified");
|
||||
|
||||
// Also update preview content
|
||||
const fileExt = fileName.split(".").slice(-1)[0];
|
||||
if (fileExt === "md") {
|
||||
setMarkdownValue(value);
|
||||
} else if (fileExt === "html") {
|
||||
setHtmlModal(value);
|
||||
}
|
||||
};
|
||||
|
||||
const saveCurrentFile = (fileName: string) => {
|
||||
if (fileName) {
|
||||
socket.emit("saveFile", {
|
||||
filename: fileName,
|
||||
content: codeValue,
|
||||
});
|
||||
const diffResult = createTwoFilesPatch(
|
||||
fileName,
|
||||
fileName,
|
||||
originalContent, // Original content
|
||||
codeValue, // Current content
|
||||
);
|
||||
|
||||
// TODO: push event
|
||||
console.warn("diffResult", diffResult);
|
||||
|
||||
// Update original content after successful save
|
||||
setOriginalContent(codeValue); // Add this line
|
||||
|
||||
// Clear save status after 3 seconds
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
setSaveStatus(null);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
// Terminal
|
||||
useEffect(() => {
|
||||
const element = terminalElementRef.current!;
|
||||
const fitAddon = new FitAddon();
|
||||
fitAddonRef.current = fitAddon;
|
||||
const terminal = new XTerm({
|
||||
fontSize: 14,
|
||||
fontFamily: 'Consolas, "Courier New", monospace',
|
||||
theme: theme === "dark" ? darkTheme : lightTheme,
|
||||
});
|
||||
// Save terminal instance reference
|
||||
terminalRef.current = terminal;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(element);
|
||||
fitAddon.fit();
|
||||
|
||||
let currentCommand = "";
|
||||
|
||||
terminal.onData((data) => {
|
||||
socket.emit("input", data);
|
||||
if (data === "\r" || data === "\n") {
|
||||
if (currentCommand.trim()) {
|
||||
const command = currentCommand.trim();
|
||||
setLastCommand(command);
|
||||
lastCommandRef.current = command;
|
||||
setIsCommandRunning(true);
|
||||
setCommandOutput(""); // Clear previous output
|
||||
currentCommand = "";
|
||||
}
|
||||
} else if (data === "\u007f") {
|
||||
// Backspace key
|
||||
currentCommand = currentCommand.slice(0, -1);
|
||||
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
||||
// Printable character
|
||||
currentCommand += data;
|
||||
}
|
||||
});
|
||||
const handleOutput = (data: string) => {
|
||||
terminal.write(data);
|
||||
|
||||
if (isCommandRunning) {
|
||||
setCommandOutput((prev) => {
|
||||
const newOutput = prev + data;
|
||||
|
||||
const cleanedOutput = newOutput
|
||||
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
||||
.replace(/\x1b\][0-9];[^\x07]*\x07/g, "")
|
||||
.replace(/\x1b\][0-9];[^\x1b]*\x1b\\/g, "")
|
||||
.replace(/\[?\?[0-9]+[hl]/g, ""); // More lenient prompt matching
|
||||
const promptPattern = /root@[a-f0-9]+:[^#]*#\s*$/;
|
||||
if (promptPattern.test(cleanedOutput)) {
|
||||
setIsCommandRunning(false);
|
||||
|
||||
const promptMatch = cleanedOutput.match(
|
||||
/root@[a-f0-9]+:[^#]*#\s*$/,
|
||||
);
|
||||
if (promptMatch) {
|
||||
const cleanOutput = cleanedOutput
|
||||
.substring(0, promptMatch.index)
|
||||
.trim();
|
||||
// TODO: push event
|
||||
console.warn(
|
||||
`Command: ${lastCommandRef.current}\nResults: ${cleanOutput}`,
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return newOutput;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("output", handleOutput);
|
||||
return () => {
|
||||
terminal.dispose();
|
||||
socket.off("connect");
|
||||
socket.off("error");
|
||||
socket.off("fileTree");
|
||||
socket.off("fileContent");
|
||||
socket.off("output", handleOutput);
|
||||
};
|
||||
}, [artifactsSio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.options.theme =
|
||||
theme === "dark" ? darkTheme : lightTheme;
|
||||
}
|
||||
}, [theme]);
|
||||
const onClose = () => {
|
||||
setModal(false);
|
||||
};
|
||||
const modalTitle = (
|
||||
<div className={styles.modalTitle}>
|
||||
<span
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{fileName}
|
||||
</span>
|
||||
{saveStatus && (
|
||||
<span
|
||||
className={classNames(styles.saveStatus, {
|
||||
[styles.modified]: saveStatus === "modified",
|
||||
[styles.saved]: saveStatus === "saved",
|
||||
})}
|
||||
>
|
||||
{saveStatus === "modified" ? "• Modified • Ctrl + S" : "• Saved"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.artifactsWrap}>
|
||||
<Modal
|
||||
title={modalTitle}
|
||||
open={modal}
|
||||
centered={true}
|
||||
// footer={null}
|
||||
onCancel={onClose}
|
||||
onOk={onClose}
|
||||
width={960}
|
||||
>
|
||||
<div className={styles.modalContent}>
|
||||
{fileName.split(".").slice(-1)[0] === "md" ? (
|
||||
<div
|
||||
className={`markdown-body ${
|
||||
theme === "dark" ? "markdown-dark" : "markdown-light"
|
||||
}`}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeHighlight, rehypeKatex]}
|
||||
components={{
|
||||
code(props: any) {
|
||||
const { node, inline, className, children, ...rest } =
|
||||
props;
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<pre>
|
||||
<code className={match[1]} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
) : (
|
||||
<code className={className} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdownValue}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : fileName.split(".").slice(-1)[0] === "html" ? (
|
||||
<div className="iframe-box">
|
||||
<iframe
|
||||
srcDoc={htmlModal}
|
||||
sandbox="allow-scripts"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CodeMirror
|
||||
value={codeValue}
|
||||
height="500px"
|
||||
extensions={[...extensions, saveKeymap]}
|
||||
onChange={handleCodeChange}
|
||||
theme={theme === "dark" ? materialDark : materialLight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<PanelGroup
|
||||
direction="vertical"
|
||||
onLayout={() => {
|
||||
fitAddonRef.current?.fit();
|
||||
}}
|
||||
>
|
||||
<Panel defaultSize={50} minSize={20}>
|
||||
<PanelGroup direction="horizontal">
|
||||
<Panel defaultSize={40} minSize={10} collapsible>
|
||||
<div
|
||||
className="flex flex-col border-r border-bolt-elements-borderColor h-full"
|
||||
style={{ backgroundColor: "transparent" }}
|
||||
>
|
||||
<PanelHeader>Files</PanelHeader>
|
||||
<div className={styles.treeWrap}>
|
||||
<FileTree
|
||||
data={fileTree}
|
||||
onExpand={onExpand}
|
||||
show={"noShow"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel className="flex flex-col" defaultSize={80} minSize={20}>
|
||||
<PanelHeader className="overflow-x-auto">
|
||||
<div className="header">
|
||||
<div className="fileName">
|
||||
<Tooltip title={fileName} placement="top">
|
||||
<span
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{fileName}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{saveStatus && (
|
||||
<span
|
||||
className={classNames(styles.saveStatus, {
|
||||
[styles.modified]: saveStatus === "modified",
|
||||
[styles.saved]: saveStatus === "saved",
|
||||
})}
|
||||
>
|
||||
{saveStatus === "modified"
|
||||
? "• Modified • Ctrl + S"
|
||||
: "• Saved"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(fileName.split(".").slice(-1)[0] === "md" ||
|
||||
fileName.split(".").slice(-1)[0] === "html") && (
|
||||
<Button
|
||||
type="link"
|
||||
style={{ flexShrink: 0 }}
|
||||
onClick={() => {
|
||||
// Update preview content before opening preview
|
||||
const fileExt = fileName.split(".").slice(-1)[0];
|
||||
if (fileExt === "md") {
|
||||
setMarkdownValue(codeValue);
|
||||
} else if (fileExt === "html") {
|
||||
setHtmlModal(codeValue);
|
||||
}
|
||||
setModal(true);
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PanelHeader>
|
||||
<div className={styles.codeWrap}>
|
||||
<CodeMirror
|
||||
value={codeValue}
|
||||
extensions={[...extensions, saveKeymap]}
|
||||
theme={theme === "dark" ? materialDark : materialLight}
|
||||
onChange={(value) => {
|
||||
setCodeValue(value);
|
||||
setSaveStatus("modified");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel defaultSize={20} minSize={10}>
|
||||
<div className="h-full" style={{ height: "100%" }}>
|
||||
<div
|
||||
className="bg-bolt-elements-terminals-background h-full flex flex-col"
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full",
|
||||
{
|
||||
"bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary":
|
||||
false,
|
||||
"bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground":
|
||||
!false,
|
||||
},
|
||||
)}
|
||||
>
|
||||
Terminal
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
<div className="terminal" ref={terminalElementRef} />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(Artifacts);
|
||||
384
alias/frontend/src/components/Browser/index.scss
Normal file
@@ -0,0 +1,384 @@
|
||||
/* CSS Variables for themes */
|
||||
html[data-theme="dark"] {
|
||||
--bg-primary: #272725;
|
||||
--bg-secondary: #171717;
|
||||
--border-color: #383838;
|
||||
--text-color: #ffffff;
|
||||
--tab-active-bg: #272725;
|
||||
--tab-hover-bg: #333333;
|
||||
--icon-color: #8a8a8a;
|
||||
--icon-hover-color: #ffffff;
|
||||
--error-color: #e53935;
|
||||
--offline-indicator-color: #e53935;
|
||||
--loading-overlay-bg: rgba(30, 30, 30, 0.8);
|
||||
--loading-spinner-color: #ffffff;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f5f5;
|
||||
--border-color: #e0e0e0;
|
||||
--text-color: #000000;
|
||||
--tab-active-bg: #e8e8e8;
|
||||
--tab-hover-bg: #efefef;
|
||||
--icon-color: #666666;
|
||||
--icon-hover-color: #000000;
|
||||
--error-color: #e53935;
|
||||
--offline-indicator-color: #e53935;
|
||||
--loading-overlay-bg: rgba(240, 240, 240, 0.8);
|
||||
--loading-spinner-color: #333333;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.browser-chrome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
gap: 4px;
|
||||
height: 36px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
align-items: center;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-color);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tab-hover-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--tab-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.address-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
height: 40px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--icon-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--icon-hover-color);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.url-bar {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-color);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
background: var(--tab-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
font-family: 'Geist', sans-serif;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.loading::before {
|
||||
content: "Loading...";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--text-color);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&.error::before {
|
||||
content: "Session released";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&.tab-switching::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--loading-overlay-bg);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&.tab-switching::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: var(--loading-spinner-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 36px;
|
||||
color: var(--text-color);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
min-width: 140px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.offline {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.online {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.offline {
|
||||
background-color: var(--offline-indicator-color);
|
||||
}
|
||||
}
|
||||
|
||||
.url-security-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: var(--icon-color);
|
||||
}
|
||||
|
||||
&.secure svg {
|
||||
fill: #4CAF50;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-favicon-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
border: 2px solid var(--icon-color);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spinner-rotation 0.8s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.tab.loading {
|
||||
.tab-favicon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-favicon-spinner {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
691
alias/frontend/src/components/Browser/index.tsx
Normal file
@@ -0,0 +1,691 @@
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import "./index.scss";
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
favicon: string | null;
|
||||
ws: WebSocket | null;
|
||||
receivedFirstFrame: boolean;
|
||||
lastImageData: string | null;
|
||||
isLoading: boolean;
|
||||
frameCount: number;
|
||||
canvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
currentImageWidth: number;
|
||||
currentImageHeight: number;
|
||||
reconnecting: boolean;
|
||||
intentionalClose: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
type ConnectionStatus = "online" | "offline" | "connecting";
|
||||
|
||||
const defaultWidth = 1920;
|
||||
const defaultHeight = 1080;
|
||||
|
||||
interface BrowserProps {
|
||||
webSocketUrl: {
|
||||
browserWs: string;
|
||||
artifactsSio?: string;
|
||||
};
|
||||
activeKey?: string;
|
||||
}
|
||||
|
||||
const Browser: React.FC<BrowserProps> = ({ webSocketUrl, activeKey }) => {
|
||||
const [tabs, setTabs] = useState<Record<string, Tab>>({});
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("connecting");
|
||||
const [tabOrder, setTabOrder] = useState<string[]>([]);
|
||||
const [isUrlBarFocused, setIsUrlBarFocused] = useState(false);
|
||||
const urlTextRef = useRef<HTMLInputElement>(null);
|
||||
const wsDiscoveryRef = useRef<WebSocket | null>(null);
|
||||
const activeConnectionRetries = useRef<Record<string, number>>({});
|
||||
const singlePageMode = false;
|
||||
const interactive = true;
|
||||
|
||||
// Initialize tab-discovery ws
|
||||
useEffect(() => {
|
||||
if (singlePageMode) return;
|
||||
const ws = new WebSocket(webSocketUrl.browserWs + "?tabInfo=true");
|
||||
wsDiscoveryRef.current = ws;
|
||||
ws.onopen = () => setConnectionStatus("online");
|
||||
ws.onclose = () => setConnectionStatus("offline");
|
||||
ws.onerror = () => setConnectionStatus("offline");
|
||||
ws.onmessage = (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (payload.type === "tabList" && payload.tabs) {
|
||||
handleTabList(payload.tabs, payload.firstTabId);
|
||||
} else if (payload.type === "tabClosed" && payload.pageId) {
|
||||
handleTabClosed(payload.pageId);
|
||||
} else if (payload.type === "activeTabChange" && payload.pageId) {
|
||||
setActiveTabId(payload.pageId);
|
||||
}
|
||||
};
|
||||
return () => ws.close();
|
||||
// eslint-disable-next-line
|
||||
}, [webSocketUrl.browserWs]);
|
||||
|
||||
// When activating tab, close previous tab's ws and create new ws
|
||||
useEffect(() => {
|
||||
if (!activeTabId) return;
|
||||
const tab = tabs[activeTabId];
|
||||
if (!tab) return;
|
||||
if (tab.ws) return; // Already has ws
|
||||
connectTabWebSocket(activeTabId);
|
||||
// eslint-disable-next-line
|
||||
}, [activeTabId, tabs]);
|
||||
|
||||
// Handle tabList, automatically create/activate tabs
|
||||
const handleTabList = useCallback((tabList: any[], firstTabId?: string) => {
|
||||
const newTabs: Record<string, Tab> = {};
|
||||
const order: string[] = [];
|
||||
tabList.forEach((tab) => {
|
||||
newTabs[tab.id] = {
|
||||
id: tab.id,
|
||||
url: tab.url,
|
||||
title: tab.title,
|
||||
favicon: tab.favicon,
|
||||
ws: null,
|
||||
receivedFirstFrame: false,
|
||||
lastImageData: null,
|
||||
isLoading: false,
|
||||
frameCount: 0,
|
||||
canvasRef: React.createRef(),
|
||||
containerRef: React.createRef(),
|
||||
currentImageWidth: defaultWidth,
|
||||
currentImageHeight: defaultHeight,
|
||||
reconnecting: false,
|
||||
intentionalClose: false,
|
||||
error: false,
|
||||
};
|
||||
order.push(tab.id);
|
||||
});
|
||||
setTabs(newTabs);
|
||||
setTabOrder(order);
|
||||
if (firstTabId && newTabs[firstTabId]) {
|
||||
setActiveTabId(firstTabId);
|
||||
} else if (tabList.length > 0) {
|
||||
setActiveTabId(tabList[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Close tab
|
||||
const handleTabClosed = useCallback(
|
||||
(pageId: string) => {
|
||||
setTabs((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (updated[pageId]?.ws) updated[pageId].ws?.close();
|
||||
delete updated[pageId];
|
||||
return updated;
|
||||
});
|
||||
setTabOrder((prev) => prev.filter((id) => id !== pageId));
|
||||
if (activeTabId === pageId) {
|
||||
const tabIds = tabOrder.filter((id) => id !== pageId);
|
||||
if (tabIds.length > 0) setActiveTabId(tabIds[0]);
|
||||
else setActiveTabId(null);
|
||||
}
|
||||
},
|
||||
[activeTabId, tabOrder],
|
||||
);
|
||||
|
||||
// Update tab information
|
||||
const updateTabInfo = useCallback(
|
||||
(pageId: string, url: string, title: string, favicon: string | null) => {
|
||||
setTabs((prev) => ({
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
url,
|
||||
title,
|
||||
favicon,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Connect tab ws
|
||||
const connectTabWebSocket = (pageId: string) => {
|
||||
setTabs((prev) => {
|
||||
if (!prev[pageId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
isLoading: true,
|
||||
error: false,
|
||||
reconnecting: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
const ws = new WebSocket(
|
||||
webSocketUrl.browserWs + `?pageId=${encodeURIComponent(pageId)}`,
|
||||
);
|
||||
ws.onopen = () => {
|
||||
setTabs((prev) => {
|
||||
if (!prev[pageId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
ws,
|
||||
isLoading: false,
|
||||
error: false,
|
||||
reconnecting: false,
|
||||
frameCount: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
setConnectionStatus("online");
|
||||
};
|
||||
ws.onclose = () => {
|
||||
setTabs((prev) => {
|
||||
if (!prev[pageId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
isLoading: false,
|
||||
error: true,
|
||||
reconnecting: false,
|
||||
ws: null,
|
||||
},
|
||||
};
|
||||
});
|
||||
setConnectionStatus("offline");
|
||||
};
|
||||
ws.onerror = () => {
|
||||
setTabs((prev) => {
|
||||
if (!prev[pageId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
isLoading: false,
|
||||
error: true,
|
||||
reconnecting: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
setConnectionStatus("offline");
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (payload.type === "tabUpdate") {
|
||||
updateTabInfo(
|
||||
pageId,
|
||||
payload.url || "",
|
||||
payload.title || "",
|
||||
payload.favicon || null,
|
||||
);
|
||||
} else if (payload.type === "targetClosed") {
|
||||
handleTabClosed(pageId);
|
||||
}
|
||||
if (payload.data) {
|
||||
renderCanvasImage(
|
||||
pageId,
|
||||
payload.data,
|
||||
payload.url,
|
||||
payload.title,
|
||||
payload.favicon,
|
||||
);
|
||||
}
|
||||
};
|
||||
setTabs((prev) => {
|
||||
if (!prev[pageId]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[pageId]: {
|
||||
...prev[pageId],
|
||||
ws,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Render image to canvas
|
||||
const renderCanvasImage = (
|
||||
pageId: string,
|
||||
imageData: string,
|
||||
url?: string,
|
||||
title?: string,
|
||||
favicon?: string,
|
||||
) => {
|
||||
setTabs((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (!updated[pageId]) return updated;
|
||||
updated[pageId].receivedFirstFrame = true;
|
||||
updated[pageId].lastImageData = imageData.startsWith(
|
||||
"data:image/jpeg;base64,",
|
||||
)
|
||||
? imageData
|
||||
: `data:image/jpeg;base64,${imageData}`;
|
||||
updated[pageId].isLoading = false;
|
||||
updated[pageId].error = false;
|
||||
if (url && !isUrlBarFocused) updated[pageId].url = url;
|
||||
if (title) updated[pageId].title = title;
|
||||
if (favicon) updated[pageId].favicon = favicon;
|
||||
updated[pageId].frameCount++;
|
||||
return updated;
|
||||
});
|
||||
setTimeout(() => {
|
||||
const tab = tabs[pageId];
|
||||
const canvas = tab?.canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d", { alpha: false });
|
||||
if (!ctx) return;
|
||||
const img = new window.Image();
|
||||
img.src = imageData.startsWith("data:image/jpeg;base64,")
|
||||
? imageData
|
||||
: `data:image/jpeg;base64,${imageData}`;
|
||||
img.onload = () => {
|
||||
setTabs((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (!updated[pageId]) return updated;
|
||||
updated[pageId].currentImageWidth = img.naturalWidth;
|
||||
updated[pageId].currentImageHeight = img.naturalHeight;
|
||||
return updated;
|
||||
});
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const container = tab?.containerRef.current;
|
||||
const targetHeight = container?.clientHeight || defaultHeight;
|
||||
const targetWidth =
|
||||
targetHeight * (img.naturalWidth / img.naturalHeight);
|
||||
canvas.width = targetWidth * dpr;
|
||||
canvas.height = targetHeight * dpr;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.scale(dpr, dpr);
|
||||
canvas.style.height = "100%";
|
||||
canvas.style.width = "auto";
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
Math.floor(canvas.width / dpr),
|
||||
Math.floor(canvas.height / dpr),
|
||||
);
|
||||
};
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Canvas native event binding
|
||||
useEffect(() => {
|
||||
if (!activeTabId || activeKey !== "3") return;
|
||||
const tab = tabs[activeTabId];
|
||||
if (!tab) return;
|
||||
const canvas = tab.canvasRef.current;
|
||||
if (!canvas) return;
|
||||
// Mouse events
|
||||
const getScaledCoordinates = (e: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = tab.currentImageWidth / rect.width;
|
||||
const scaleY = tab.currentImageHeight / rect.height;
|
||||
return {
|
||||
x: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.round((e.clientX - rect.left) * scaleX),
|
||||
tab.currentImageWidth,
|
||||
),
|
||||
),
|
||||
y: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.round((e.clientY - rect.top) * scaleY),
|
||||
tab.currentImageHeight,
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
const handleMouse = (e: MouseEvent, type: string) => {
|
||||
if (!tab.ws || tab.ws.readyState !== WebSocket.OPEN) return;
|
||||
const coords = getScaledCoordinates(e);
|
||||
const modifiers =
|
||||
(e.ctrlKey ? 2 : 0) |
|
||||
(e.shiftKey ? 8 : 0) |
|
||||
(e.altKey ? 1 : 0) |
|
||||
(e.metaKey ? 4 : 0);
|
||||
let button = "none";
|
||||
if (type === "mousePressed" || type === "mouseReleased") {
|
||||
button = e.button === 0 ? "left" : e.button === 1 ? "middle" : "right";
|
||||
}
|
||||
|
||||
const eventData = JSON.stringify({
|
||||
type: "mouseEvent",
|
||||
pageId: activeTabId,
|
||||
event: {
|
||||
type,
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
button,
|
||||
modifiers,
|
||||
clickCount: (e as any).detail || 1,
|
||||
},
|
||||
});
|
||||
|
||||
// console.warn("Mouse Event:", {
|
||||
// eventString: eventData,
|
||||
// currentUrl: tab.url,
|
||||
// pageTitle: tab.title,
|
||||
// currentBase64Data: tab.lastImageData,
|
||||
// });
|
||||
|
||||
tab.ws.send(eventData);
|
||||
};
|
||||
let moveTimeout: any = null;
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (moveTimeout) clearTimeout(moveTimeout);
|
||||
moveTimeout = setTimeout(() => handleMouse(e, "mouseMoved"), 20);
|
||||
};
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (!tab.ws || tab.ws.readyState !== WebSocket.OPEN) return;
|
||||
const coords = getScaledCoordinates(e as any);
|
||||
const modifiers =
|
||||
(e.ctrlKey ? 2 : 0) |
|
||||
(e.shiftKey ? 8 : 0) |
|
||||
(e.altKey ? 1 : 0) |
|
||||
(e.metaKey ? 4 : 0);
|
||||
|
||||
const eventData = JSON.stringify({
|
||||
type: "mouseEvent",
|
||||
pageId: activeTabId,
|
||||
event: {
|
||||
type: "mouseWheel",
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
button: "none",
|
||||
modifiers,
|
||||
deltaX: e.deltaX,
|
||||
deltaY: e.deltaY,
|
||||
},
|
||||
});
|
||||
|
||||
// console.warn("Wheel Event:", {
|
||||
// eventString: eventData,
|
||||
// currentUrl: tab.url,
|
||||
// pageTitle: tab.title,
|
||||
// currentBase64Data: tab.lastImageData,
|
||||
// });
|
||||
|
||||
tab.ws.send(eventData);
|
||||
e.preventDefault();
|
||||
};
|
||||
canvas.addEventListener("mousedown", (e) => handleMouse(e, "mousePressed"));
|
||||
canvas.addEventListener("mouseup", (e) => handleMouse(e, "mouseReleased"));
|
||||
canvas.addEventListener("mousemove", handleMouseMove);
|
||||
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
||||
// Keyboard events (global)
|
||||
const handleKey = (e: KeyboardEvent, type: "keyDown" | "keyUp") => {
|
||||
if (document.activeElement === urlTextRef.current) return;
|
||||
if (!tab.ws || tab.ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const eventData = JSON.stringify({
|
||||
type: "keyEvent",
|
||||
pageId: activeTabId,
|
||||
event: {
|
||||
type,
|
||||
text: e.key.length === 1 ? e.key : undefined,
|
||||
code: e.code,
|
||||
key: e.key,
|
||||
keyCode: e.keyCode,
|
||||
},
|
||||
});
|
||||
|
||||
// console.warn("Browser Keyboard Event:", {
|
||||
// eventString: eventData,
|
||||
// currentUrl: tab.url,
|
||||
// pageTitle: tab.title,
|
||||
// currentBase64Data: tab.lastImageData,
|
||||
// key: e.key,
|
||||
// code: e.code,
|
||||
// });
|
||||
|
||||
tab.ws.send(eventData);
|
||||
};
|
||||
const keydown = (e: KeyboardEvent) => handleKey(e, "keyDown");
|
||||
const keyup = (e: KeyboardEvent) => handleKey(e, "keyUp");
|
||||
document.addEventListener("keydown", keydown);
|
||||
document.addEventListener("keyup", keyup);
|
||||
return () => {
|
||||
canvas.removeEventListener("mousedown", (e) =>
|
||||
handleMouse(e, "mousePressed"),
|
||||
);
|
||||
canvas.removeEventListener("mouseup", (e) =>
|
||||
handleMouse(e, "mouseReleased"),
|
||||
);
|
||||
canvas.removeEventListener("mousemove", handleMouseMove);
|
||||
canvas.removeEventListener("wheel", handleWheel);
|
||||
document.removeEventListener("keydown", keydown);
|
||||
document.removeEventListener("keyup", keyup);
|
||||
};
|
||||
}, [activeTabId, tabs]);
|
||||
|
||||
// Address bar input
|
||||
const handleUrlSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!urlTextRef.current || !activeTabId) return;
|
||||
const url = urlTextRef.current.value;
|
||||
handleNavigation("url", url);
|
||||
urlTextRef.current.blur();
|
||||
};
|
||||
|
||||
// Navigation buttons
|
||||
const handleNavigation = (
|
||||
action: "back" | "forward" | "refresh" | "url",
|
||||
url?: string,
|
||||
) => {
|
||||
if (!activeTabId || !tabs[activeTabId]?.ws) return;
|
||||
const ws = tabs[activeTabId].ws;
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
setTabs((prev) => ({
|
||||
...prev,
|
||||
[activeTabId]: {
|
||||
...prev[activeTabId],
|
||||
isLoading: true,
|
||||
frameCount: 0,
|
||||
},
|
||||
}));
|
||||
const eventData = JSON.stringify({
|
||||
type: "navigation",
|
||||
pageId: activeTabId,
|
||||
event: action === "url" ? { url } : { action },
|
||||
});
|
||||
|
||||
// TODO: push event
|
||||
console.warn("Navigation Event:", {
|
||||
eventString: eventData,
|
||||
currentUrl: tabs[activeTabId].url,
|
||||
pageTitle: tabs[activeTabId].title,
|
||||
currentBase64Data: tabs[activeTabId].lastImageData,
|
||||
action,
|
||||
targetUrl: url,
|
||||
});
|
||||
|
||||
ws.send(eventData);
|
||||
if (action === "url" && url) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "navigation",
|
||||
url,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Security lock icon
|
||||
const isSecure = (url: string) =>
|
||||
url &&
|
||||
(url.toLowerCase().startsWith("https://") ||
|
||||
url.toLowerCase().startsWith("https:"));
|
||||
|
||||
// UI
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="browser-chrome">
|
||||
<div className="tab-bar" id="tab-bar">
|
||||
<div
|
||||
className={`connection-status ${connectionStatus}`}
|
||||
id="connection-status"
|
||||
>
|
||||
<div className={`status-indicator ${connectionStatus}`}></div>
|
||||
<span>
|
||||
{connectionStatus === "online"
|
||||
? "Session Online"
|
||||
: connectionStatus === "offline"
|
||||
? "Session Offline"
|
||||
: "Session Connecting..."}
|
||||
</span>
|
||||
</div>
|
||||
{tabOrder.map((id) => {
|
||||
const tab = tabs[id];
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`tab${activeTabId === id ? " active" : ""}${
|
||||
tab.isLoading ? " loading" : ""
|
||||
}`}
|
||||
onClick={() => setActiveTabId(id)}
|
||||
>
|
||||
<img
|
||||
className="tab-favicon"
|
||||
src={tab.favicon || ""}
|
||||
style={{ display: tab.favicon ? "block" : "none" }}
|
||||
alt=""
|
||||
/>
|
||||
<div className="tab-favicon-spinner"></div>
|
||||
<div className="tab-title">{tab.title || "New Tab"}</div>
|
||||
<div
|
||||
className="tab-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTabClosed(id);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="address-bar">
|
||||
<div className="nav-buttons">
|
||||
<button
|
||||
className="nav-button"
|
||||
onClick={() => handleNavigation("back")}
|
||||
disabled={!activeTabId}
|
||||
>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="nav-button"
|
||||
onClick={() => handleNavigation("forward")}
|
||||
disabled={!activeTabId}
|
||||
>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="nav-button"
|
||||
onClick={() => handleNavigation("refresh")}
|
||||
disabled={!activeTabId}
|
||||
>
|
||||
<svg className="icon" viewBox="0 0 24 24">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form className="url-bar" onSubmit={handleUrlSubmit}>
|
||||
<div
|
||||
className={`url-security-icon${
|
||||
isSecure(tabs[activeTabId || ""]?.url || "") ? " secure" : ""
|
||||
}`}
|
||||
id="url-security-icon"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
id="lock-icon"
|
||||
style={{
|
||||
display: isSecure(tabs[activeTabId || ""]?.url || "")
|
||||
? "block"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" />
|
||||
</svg>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
id="unlock-icon"
|
||||
style={{
|
||||
display: isSecure(tabs[activeTabId || ""]?.url || "")
|
||||
? "none"
|
||||
: "block",
|
||||
}}
|
||||
>
|
||||
<path d="M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="url-text"
|
||||
className="url-input"
|
||||
ref={urlTextRef}
|
||||
value={tabs[activeTabId || ""]?.url || ""}
|
||||
onChange={(e) => {
|
||||
if (!activeTabId || activeKey !== "3") return;
|
||||
setTabs((prev) => ({
|
||||
...prev,
|
||||
[activeTabId]: {
|
||||
...prev[activeTabId],
|
||||
url: e.target.value,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
onFocus={() => setIsUrlBarFocused(true)}
|
||||
onBlur={() => setIsUrlBarFocused(false)}
|
||||
disabled={!activeTabId}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content">
|
||||
{tabOrder.map((id) => {
|
||||
const tab = tabs[id];
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
ref={tab.containerRef}
|
||||
className={`canvas-container${
|
||||
activeTabId === id ? " active" : ""
|
||||
}${tab.isLoading ? " loading" : ""}${tab.error ? " error" : ""}`}
|
||||
style={{
|
||||
display: activeTabId === id ? "flex" : "none",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={tab.canvasRef}
|
||||
className="canvas"
|
||||
width={defaultWidth}
|
||||
height={defaultHeight}
|
||||
style={{ height: "100%", width: "auto" }}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Browser);
|
||||
80
alias/frontend/src/components/Chat/BaseMessage.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Message, MessageRole, MessageState } from "@/types/message";
|
||||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import styles from "./Message.module.scss";
|
||||
|
||||
interface BaseMessageProps {
|
||||
message: Message;
|
||||
children: React.ReactNode;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const BaseMessage: React.FC<BaseMessageProps> = ({
|
||||
message,
|
||||
children,
|
||||
onFeedback,
|
||||
}) => {
|
||||
const isAssistant = message.role === MessageRole.ASSISTANT;
|
||||
const isRunning = message.status === MessageState.RUNNING;
|
||||
const isWaiting = message.status === MessageState.WAITING;
|
||||
const isError = message.status === MessageState.ERROR;
|
||||
|
||||
const renderContent = (content: React.ReactNode) => {
|
||||
if (typeof content === "string") {
|
||||
return (
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.message} ${
|
||||
isAssistant ? styles.assistant : styles.user
|
||||
}`}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
{renderContent(children)}
|
||||
{isRunning && <div className={styles.loading}>...</div>}
|
||||
{isWaiting && (
|
||||
<div className={styles.waiting}>Waiting for user selection...</div>
|
||||
)}
|
||||
{isError && <div className={styles.error}>An error occurred</div>}
|
||||
</div>
|
||||
{isAssistant && onFeedback && (
|
||||
<div className={styles.feedback}>
|
||||
<button
|
||||
className={`${styles.feedbackBtn} ${
|
||||
message.feedback === "like" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() =>
|
||||
onFeedback(
|
||||
message.id,
|
||||
message.feedback === "like" ? null : "like",
|
||||
)
|
||||
}
|
||||
>
|
||||
👍
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.feedbackBtn} ${
|
||||
message.feedback === "dislike" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() =>
|
||||
onFeedback(
|
||||
message.id,
|
||||
message.feedback === "dislike" ? null : "dislike",
|
||||
)
|
||||
}
|
||||
>
|
||||
👎
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
79
alias/frontend/src/components/Chat/ClarificationMessage.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ClarificationMessage as ClarificationType,
|
||||
SelectionType,
|
||||
} from "@/types/message";
|
||||
import React, { useState } from "react";
|
||||
import styles from "./Message.module.scss";
|
||||
|
||||
interface ClarificationMessageProps {
|
||||
message: ClarificationType;
|
||||
onSelect?: (options: string[]) => void;
|
||||
}
|
||||
|
||||
export const ClarificationMessage: React.FC<ClarificationMessageProps> = ({
|
||||
message,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
||||
|
||||
const handleOptionClick = (option: string) => {
|
||||
if (message.selection_type === SelectionType.SINGLE) {
|
||||
setSelectedOptions([option]);
|
||||
onSelect?.([option]);
|
||||
} else {
|
||||
const newSelection = selectedOptions.includes(option)
|
||||
? selectedOptions.filter((item) => item !== option)
|
||||
: [...selectedOptions, option];
|
||||
setSelectedOptions(newSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (
|
||||
message.selection_type === SelectionType.MULTIPLE &&
|
||||
selectedOptions.length > 0
|
||||
) {
|
||||
onSelect?.(selectedOptions);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
// <BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div className={styles.clarificationMessage}>
|
||||
{message.content && (
|
||||
<div className={styles.question}>{message.content}</div>
|
||||
)}
|
||||
{Array.isArray(message?.options) && message.options.length > 0 && (
|
||||
<div className={styles.options}>
|
||||
{message.options?.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`${styles.option} ${
|
||||
selectedOptions.includes(option) ? styles.selected : ""
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOptionClick(option);
|
||||
}}
|
||||
disabled={
|
||||
selectedOptions.length > 0 && !selectedOptions.includes(option)
|
||||
}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{message.selection_type === SelectionType.MULTIPLE && (
|
||||
<button
|
||||
className={styles.confirmButton}
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedOptions.length === 0}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
// {/* </BaseMessage> */}
|
||||
);
|
||||
};
|
||||
83
alias/frontend/src/components/Chat/CollapsibleMessage.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import { SparkDoubleRightLine } from "@agentscope-ai/icons";
|
||||
import styles from "./Message.module.scss";
|
||||
import type { Message as MessageType } from "@/types/message";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { markdownRegex, codeBlockRegex } from "@/utils/constant";
|
||||
|
||||
const CollapsibleMessage: React.FC<{ message: MessageType }> = ({
|
||||
message,
|
||||
}) => {
|
||||
const messageContent = message.content;
|
||||
|
||||
const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const handleExpand = (messageId: string) => {
|
||||
setExpandedStates((prev) => ({
|
||||
...prev,
|
||||
[messageId]: !prev[messageId],
|
||||
}));
|
||||
};
|
||||
if (messageContent === null || messageContent === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof messageContent !== "string") {
|
||||
return JSON.stringify(messageContent, null, 2);
|
||||
}
|
||||
|
||||
let content = messageContent;
|
||||
|
||||
if (markdownRegex.test(messageContent)) {
|
||||
content = messageContent?.match(markdownRegex)?.[1] || messageContent;
|
||||
} else if (codeBlockRegex.test(messageContent)) {
|
||||
content = messageContent?.match(codeBlockRegex)?.[1] || messageContent;
|
||||
}
|
||||
|
||||
const lines = content.split("\n");
|
||||
const showLines = 10;
|
||||
const isExpanded = expandedStates[message.id];
|
||||
|
||||
// Determine if we should show full content
|
||||
const shouldShowFullContent =
|
||||
message.isGenerating || lines.length <= showLines || isExpanded;
|
||||
|
||||
// Calculate visible content
|
||||
const visibleContent = shouldShowFullContent
|
||||
? content
|
||||
: lines.slice(0, showLines).join("\n");
|
||||
|
||||
return (
|
||||
<div className={styles.collapsibleContent}>
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{visibleContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{!shouldShowFullContent && (
|
||||
<div
|
||||
className={`${styles.gradientWrapper} ${
|
||||
isExpanded ? styles.expanded : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className={styles.collapseButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleExpand(message.id);
|
||||
}}
|
||||
>
|
||||
<SparkDoubleRightLine
|
||||
className={`${styles.arrow} ${
|
||||
isExpanded ? styles.up : styles.down
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(CollapsibleMessage);
|
||||
145
alias/frontend/src/components/Chat/FileItems.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import iconDoc from "@/assets/icons/files/doc.svg";
|
||||
import iconFile from "@/assets/icons/files/file.svg";
|
||||
import iconHtml from "@/assets/icons/files/html.svg";
|
||||
import iconImage from "@/assets/icons/files/image.svg";
|
||||
import iconPdf from "@/assets/icons/files/pdf.svg";
|
||||
import iconXml from "@/assets/icons/files/xml.svg";
|
||||
import { FileItem } from "@/types/message";
|
||||
import { formatFileSize } from "@/utils/fileNameUtils";
|
||||
import React from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
|
||||
import styles from "./Message.module.scss";
|
||||
|
||||
interface FileItemsProps {
|
||||
files: FileItem[];
|
||||
}
|
||||
|
||||
const getFileIcon = (filename: string) => {
|
||||
if (!filename) return iconFile;
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case "jpg":
|
||||
return iconImage;
|
||||
case "jpeg":
|
||||
return iconImage;
|
||||
case "png":
|
||||
return iconImage;
|
||||
case "gif":
|
||||
return iconImage;
|
||||
case "pdf":
|
||||
return iconPdf;
|
||||
case "doc":
|
||||
return iconDoc;
|
||||
case "docx":
|
||||
return iconDoc;
|
||||
case "html":
|
||||
return iconHtml;
|
||||
case "xml":
|
||||
return iconXml;
|
||||
default:
|
||||
return iconFile;
|
||||
}
|
||||
};
|
||||
|
||||
const getFileTypeText = (filename: string) => {
|
||||
if (!filename) return "FILE";
|
||||
const ext = filename.split(".").pop()?.toUpperCase();
|
||||
if (ext === "XLS" || ext === "XLSX") return "XLS";
|
||||
if (ext === "PDF") return "PDF";
|
||||
if (ext === "DOC" || ext === "DOCX") return "DOC";
|
||||
if (["JPG", "JPEG", "PNG", "GIF"].includes(ext || "")) return "IMG";
|
||||
return ext || "FILE";
|
||||
};
|
||||
|
||||
export const FileItems: React.FC<FileItemsProps> = ({ files }) => {
|
||||
const location = useLocation();
|
||||
const isSharePage = location.pathname.includes("/share/");
|
||||
const { sessionId, userId } = useParams<{
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
}>();
|
||||
if (!files || !Array.isArray(files) || files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const handlePreview = (file: FileItem) => {
|
||||
if (!file.id) return;
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:8000";
|
||||
// const endpoint = isSharePage ? "public" : "preview";
|
||||
// const url = `${baseUrl}/api/v1/files/${file.id}/${endpoint}`;
|
||||
let url: string;
|
||||
if (isSharePage && userId && sessionId) {
|
||||
url = `${baseUrl}/api/v1/share/conversations/${userId}/${sessionId}/files/${file.id}/public`;
|
||||
} else {
|
||||
url = `${baseUrl}/api/v1/files/${file.id}/preview`;
|
||||
}
|
||||
// If not share page, need to add authentication header
|
||||
if (!isSharePage) {
|
||||
const token =
|
||||
localStorage.getItem("access_token") ||
|
||||
import.meta.env.VITE_API_ACCESS_TOKEN ||
|
||||
import.meta.env.VITE_API_TOKEN;
|
||||
if (!token) {
|
||||
console.error("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a request with authentication header
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
window.open(objectUrl, "_blank");
|
||||
// Clean up URL
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to preview file:", error);
|
||||
});
|
||||
} else {
|
||||
// Share page opens link directly
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{files.map((file, index) => (
|
||||
<div key={index} className={styles.fileItem}>
|
||||
<div className={styles.fileLeft}>
|
||||
<img
|
||||
src={getFileIcon(file?.filename || "")}
|
||||
className={styles.fileIcon}
|
||||
alt="file icon"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fileInfoBlock}>
|
||||
<div className={styles.fileName} title={file?.filename}>
|
||||
{file?.filename || "Unknown file"}
|
||||
</div>
|
||||
<div className={styles.fileSize}>
|
||||
{getFileTypeText(file?.filename || "")}{" "}
|
||||
{formatFileSize(file?.size || 0)}
|
||||
</div>
|
||||
</div>
|
||||
{file?.id && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreview(file);
|
||||
}}
|
||||
className={styles.downloadBtn}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
25
alias/frontend/src/components/Chat/FilesMessage.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { FilesMessage as FilesMessageType } from "@/types/message";
|
||||
import { BaseMessage } from "./BaseMessage";
|
||||
import styles from "./Message.module.scss";
|
||||
import { FileItems } from "./FileItems";
|
||||
|
||||
interface FilesMessageProps {
|
||||
message: FilesMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const FilesMessage: React.FC<FilesMessageProps> = ({
|
||||
message,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div className={styles.filesMessage}>
|
||||
<div className={styles.filesList}>
|
||||
<FileItems files={message.files} />
|
||||
</div>
|
||||
</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
};
|
||||
888
alias/frontend/src/components/Chat/Message.module.scss
Normal file
@@ -0,0 +1,888 @@
|
||||
.messageWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.messageContent {
|
||||
flex: 1;
|
||||
width: 100%; // Ensure container does not exceed parent width
|
||||
max-width: 100%; // Limit maximum width
|
||||
overflow-x: hidden; // Prevent content overflow
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.assistantTitle {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.response {
|
||||
font-family: "Alibaba PuHuiTi 2.0";
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.2px;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
width: 100%; // Ensure container does not exceed parent width
|
||||
max-width: 100%; // Limit maximum width
|
||||
overflow-x: hidden; // Prevent content overflow
|
||||
}
|
||||
|
||||
.thought {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #a6a6a6;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #a6a6a6;
|
||||
margin: 4px 0 4px 4px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
// Add a container to wrap all sub_response
|
||||
.subResponsesContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%; // Ensure container does not exceed parent width
|
||||
max-width: 100%; // Limit maximum width
|
||||
overflow-x: hidden; // Prevent content overflow
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.subResponse {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02); // Add hover effect
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: auto;
|
||||
order: -1;
|
||||
|
||||
img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.expanded img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.subMessageItem {
|
||||
margin-left: 32px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.thought {
|
||||
padding: 8px;
|
||||
background: var(--sidebar-conversation-bg);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toolCallWrapper {
|
||||
// margin-left: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.responseGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.subMessages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.subResponseContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.messageItem {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
margin: 4px 0;
|
||||
|
||||
&.selectedMessage {
|
||||
background-color: var(--message-selected-bg, rgba(37, 99, 235, 0.05));
|
||||
border-color: var(--message-selected-border, #2563eb);
|
||||
}
|
||||
|
||||
// Can add mouse hover effect
|
||||
&:hover {
|
||||
background-color: var(--message-hover-bg, rgba(37, 99, 235, 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agentContentWrapper {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mainResponse {
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.toolCall {
|
||||
margin-left: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.expandableContent {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
opacity: 0;
|
||||
|
||||
&.expanded {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
transition: max-height 0.3s ease-in, opacity 0.2s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.toolSection,
|
||||
.fileSection,
|
||||
.thoughtSection,
|
||||
.clarificationSection {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.thoughtSection {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.fileSection {
|
||||
.fileItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clarificationSection {
|
||||
.optionsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.optionButton {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-weight: 500;
|
||||
|
||||
&:disabled {
|
||||
opacity: 1; // Selected button remains fully opaque
|
||||
}
|
||||
}
|
||||
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.messageFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-left: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
&.running {
|
||||
background: #e6f4ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback {
|
||||
display: flex;
|
||||
flex-direction: row; // Changed to left to right
|
||||
justify-content: flex-start; // Ensure left alignment
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filesMessage {
|
||||
padding: 16px 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.filesList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.fileItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--message-fileItem-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 6px 0 rgba(99, 102, 241, 0.06);
|
||||
padding: 10px 18px;
|
||||
min-width: 200px;
|
||||
max-width: 260px;
|
||||
margin-bottom: 0;
|
||||
transition: box-shadow 0.2s, border 0.2s, background 0.2s;
|
||||
border: 1px solid var(--sps-color-border-secondary);
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: var(--message-content);
|
||||
box-shadow: 0 2px 12px 0 rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.fileLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.fileInfoBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fileName {
|
||||
font-weight: 600;
|
||||
color: var(--message-fileItem-text);
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
color: #60a5fa;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 10px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
color: #6366f1;
|
||||
font-size: 13px;
|
||||
margin-top: auto;
|
||||
transition: color 0.2s;
|
||||
&:hover {
|
||||
color: #4338ca;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
// Basic text styles
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
// Ensure container does not exceed parent width
|
||||
width: 100%;
|
||||
// Limit maximum width
|
||||
max-width: 100%;
|
||||
|
||||
// Heading styles
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0.3em 0 0.1em;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
// Paragraph styles
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
&+p {
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// List styles
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em 0;
|
||||
padding-left: 1.2em;
|
||||
}
|
||||
|
||||
li {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&+li {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
// Code styles
|
||||
code {
|
||||
padding: 0.1em 0.2em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||
Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
margin: 0.2em 0;
|
||||
font-size: 85%;
|
||||
line-height: 1.4;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
// display: block; // Make code block occupy a single line
|
||||
width: 100%;
|
||||
// Allow code to wrap
|
||||
white-space: pre-wrap;
|
||||
// Ensure long words can wrap
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
// Quote styles
|
||||
blockquote {
|
||||
margin: 0.2em 0;
|
||||
padding: 0 0.5em;
|
||||
color: #656d76;
|
||||
border-left: 0.2em solid #d0d7de;
|
||||
}
|
||||
|
||||
// Table styles
|
||||
table {
|
||||
margin: 0.2em 0;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #d0d7de;
|
||||
|
||||
&:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image styles
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
// Divider styles
|
||||
hr {
|
||||
height: 1px;
|
||||
margin: 0.2em 0;
|
||||
background-color: #d0d7de;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Link styles
|
||||
a {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Specifically handle spaces created by line breaks
|
||||
br {
|
||||
// Changed to inline display
|
||||
display: inline;
|
||||
// Preserve line break effect
|
||||
white-space: pre;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 0.3em;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// Handle consecutive line breaks
|
||||
br+br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Handle line breaks in paragraphs
|
||||
p {
|
||||
br {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
height: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle line breaks in list items
|
||||
li {
|
||||
br {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
height: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
// Task list styles
|
||||
input[type="checkbox"] {
|
||||
margin: 0 0.3em 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Task list item styles
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
margin-left: -1.2em;
|
||||
padding-left: 1.2em;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.2em;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove extra spacing from first and last elements
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.thinkingContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.thinkingDots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #666;
|
||||
border-radius: 50%;
|
||||
animation: thinking 1.4s infinite ease-in-out;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thinkingText {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes thinking {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.responseGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.response {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.subMessages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.clarificationMessage {
|
||||
border-radius: 20px;
|
||||
opacity: 1;
|
||||
background: var(--message-clarification-bg);
|
||||
padding: 11px 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.question {
|
||||
font-family: PingFang SC;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
font-variation-settings: "opsz" auto;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
|
||||
.option {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
opacity: 1;
|
||||
background: var(--message-option-bg);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--message-option-border);
|
||||
padding: 10px 30px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--message-option-bg-hover);
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--message-option-bg-hover);
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.confirmButton {
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: #2563eb;
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapsibleContent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradientWrapper {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
rgba(255, 255, 255, 0.3) 100%);
|
||||
}
|
||||
|
||||
.collapseButton {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.arrow.up {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.arrow.down {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Mouse hover effect */
|
||||
.collapseButton:hover .arrow {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
266
alias/frontend/src/components/Chat/Message.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import AssistantAvatar from "@/assets/icons/avatar/assistantHeader.png";
|
||||
import correctCheckIcon from "@/assets/icons/check/correct-checkCircle-line.svg";
|
||||
import pendingCheckIcon from "@/assets/icons/check/pending-checkCircle-line.svg";
|
||||
import type {
|
||||
ClarificationMessage,
|
||||
FilesMessage,
|
||||
Message as MessageType,
|
||||
ToolCallMessage,
|
||||
} from "@/types/message";
|
||||
import {
|
||||
MessageRole,
|
||||
MessageState,
|
||||
MessageType as MsgType,
|
||||
} from "@/types/message";
|
||||
import { Flex } from "antd";
|
||||
import React, { memo, useMemo, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { ClarificationMessage as ClarificationMessageComponent } from "./ClarificationMessage";
|
||||
import CollapsibleMessage from "./CollapsibleMessage";
|
||||
import { FilesMessage as FilesMessageComponent } from "./FilesMessage";
|
||||
import styles from "./Message.module.scss";
|
||||
import { ToolCallMessage as ToolCallMessageComponent } from "./ToolCallMessage";
|
||||
import { UserMessage } from "./UserMessage";
|
||||
|
||||
interface MessageProps {
|
||||
message: MessageType;
|
||||
onClarificationSelect?: (options: string[]) => void;
|
||||
isReplayMode?: boolean;
|
||||
isGenerating?: boolean;
|
||||
messages?: MessageType[];
|
||||
}
|
||||
|
||||
const deduplicateMessages = (messages: MessageType[]): MessageType[] => {
|
||||
const result: MessageType[] = [];
|
||||
let consecutiveSubResponses: MessageType[] = [];
|
||||
|
||||
// Handle consecutive SUB_RESPONSE
|
||||
const processConsecutiveSubResponses = () => {
|
||||
if (consecutiveSubResponses.length > 0) {
|
||||
// Deduplicate consecutive SUB_RESPONSE
|
||||
const uniqueMessages = new Map<string, MessageType>();
|
||||
consecutiveSubResponses.forEach((msg) => {
|
||||
const key = `${msg.name}-${msg.content}`;
|
||||
if (uniqueMessages.has(key)) {
|
||||
const existingMsg = uniqueMessages.get(key)!;
|
||||
if (
|
||||
new Date(msg.create_time).getTime() >
|
||||
new Date(existingMsg.create_time).getTime()
|
||||
) {
|
||||
uniqueMessages.set(key, msg);
|
||||
}
|
||||
} else {
|
||||
uniqueMessages.set(key, msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Add deduplicated messages to result
|
||||
result.push(...Array.from(uniqueMessages.values()));
|
||||
consecutiveSubResponses = []; // Clear temporary array
|
||||
}
|
||||
};
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
if (msg.type === MsgType.SUB_RESPONSE) {
|
||||
consecutiveSubResponses.push(msg);
|
||||
} else {
|
||||
// When encountering non-SUB_RESPONSE message, process previously collected consecutive SUB_RESPONSE
|
||||
processConsecutiveSubResponses();
|
||||
// Add non-SUB_RESPONSE message
|
||||
result.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
// Process last group of consecutive SUB_RESPONSE
|
||||
processConsecutiveSubResponses();
|
||||
|
||||
return result;
|
||||
};
|
||||
export const Message: React.FC<MessageProps> = ({
|
||||
message,
|
||||
onClarificationSelect,
|
||||
isReplayMode = false,
|
||||
isGenerating = false,
|
||||
messages = [],
|
||||
}) => {
|
||||
const isUser = message.role === MessageRole.USER;
|
||||
const [openPopoverId, setOpenPopoverId] = useState<string | null>(null);
|
||||
const location = useLocation();
|
||||
const isSharePage = location.pathname.includes("/share/");
|
||||
|
||||
const renderAvatar = () => {
|
||||
return (
|
||||
<Flex gap="middle">
|
||||
<div className={styles.avatar}>
|
||||
<img src={AssistantAvatar} alt="Assistant" />
|
||||
</div>
|
||||
<div className={styles.assistantTitle}>Alias Agent</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
// Render directly if it's a user message
|
||||
if (isUser && message.type === MsgType.USER)
|
||||
return <UserMessage message={message} />;
|
||||
// For agent messages, only render the entire group for the first message
|
||||
const isFirstMessage =
|
||||
messages.find(
|
||||
(msg) =>
|
||||
msg.role === MessageRole.ASSISTANT &&
|
||||
msg.parent_message_id === message.parent_message_id,
|
||||
)?.id === message.id;
|
||||
|
||||
if (!isFirstMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Modify renderStatus function
|
||||
const renderStatus = () => {
|
||||
if (!message.isGenerating) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
message.status !== MessageState.WAITING &&
|
||||
message.status !== MessageState.ERROR
|
||||
) {
|
||||
return (
|
||||
<div className={`${styles.status} ${styles.running}`}>
|
||||
Generating...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`${styles.status} ${styles[message.status.toLowerCase()]}`}
|
||||
>
|
||||
{message.status === MessageState.WAITING && "Waiting..."}
|
||||
{message.status === MessageState.ERROR && "Generation failed"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSingleMessage = (
|
||||
msg: MessageType,
|
||||
allMessages: MessageType[],
|
||||
) => {
|
||||
switch (msg.type) {
|
||||
case MsgType.RESPONSE:
|
||||
return (
|
||||
<div key={msg.id} className={styles.response}>
|
||||
<div className={styles.content}>
|
||||
<CollapsibleMessage message={msg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MsgType.THOUGHT:
|
||||
return (
|
||||
<div key={msg.id} className={styles.thought}>
|
||||
<CollapsibleMessage message={msg} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case MsgType.SUB_RESPONSE:
|
||||
return (
|
||||
<div key={msg.id}>
|
||||
<div className={styles.subResponse}>
|
||||
<img
|
||||
src={
|
||||
msg.status === MessageState.FINISHED
|
||||
? correctCheckIcon
|
||||
: pendingCheckIcon
|
||||
}
|
||||
alt="status"
|
||||
className={styles.icon}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<CollapsibleMessage message={msg} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MsgType.SUB_THOUGHT:
|
||||
return (
|
||||
<div className={styles.subMessageItem}>
|
||||
<div key={msg.id} className={styles.thought}>
|
||||
<CollapsibleMessage message={msg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case MsgType.TOOL_CALL:
|
||||
case MsgType.TOOL_USE:
|
||||
case MsgType.TOOL_RESULT:
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={styles.toolCallWrapper}
|
||||
id={`message-toolcall-${msg.id}`}
|
||||
>
|
||||
<ToolCallMessageComponent message={msg as ToolCallMessage} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case MsgType.CLARIFICATION:
|
||||
if (!msg.content && (msg?.options?.length === 0 || !msg?.options))
|
||||
return null;
|
||||
return (
|
||||
<div key={msg.id} className={styles.clarificationMessage}>
|
||||
<ClarificationMessageComponent
|
||||
message={msg as ClarificationMessage}
|
||||
onSelect={onClarificationSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MsgType.FILES:
|
||||
return (
|
||||
<div key={msg.id} className={styles.fileSection}>
|
||||
<FilesMessageComponent message={msg as FilesMessage} />
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get all non-user messages with the same parent_message_id
|
||||
const relatedMessages = useMemo(() => {
|
||||
const filteredMessages = messages.filter(
|
||||
(msg) =>
|
||||
msg.role === MessageRole.ASSISTANT &&
|
||||
msg.parent_message_id === message.parent_message_id,
|
||||
);
|
||||
|
||||
// Deduplicate messages
|
||||
const uniqueMessages = deduplicateMessages(filteredMessages);
|
||||
return uniqueMessages;
|
||||
}, [messages, message.parent_message_id]);
|
||||
|
||||
if (relatedMessages && relatedMessages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.messageWrapper}>
|
||||
{renderAvatar()}
|
||||
<div className={styles.messageContent}>
|
||||
{relatedMessages.map((msg, index) => {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.messageItem} ${
|
||||
openPopoverId === msg.id ? styles.selectedMessage : ""
|
||||
}`}
|
||||
key={msg.id}
|
||||
>
|
||||
{renderSingleMessage(msg, relatedMessages)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.messageFooter}>{renderStatus()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(Message);
|
||||
21
alias/frontend/src/components/Chat/MessageList.module.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.messageList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
alias/frontend/src/components/Chat/MessageList.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { MessageState, Message as MessageType } from "@/types/message";
|
||||
import { isAtBottom } from "@/utils/sharedRefs";
|
||||
import React, {
|
||||
memo,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Message from "./Message";
|
||||
import styles from "./MessageList.module.scss";
|
||||
import { ThinkingMessage } from "./ThinkingMessage";
|
||||
|
||||
interface MessageListProps {
|
||||
messages?: MessageType[];
|
||||
onClarificationSelect?: (options: string[]) => void;
|
||||
onAddMessage?: (message: MessageType) => void;
|
||||
isThinking?: boolean;
|
||||
isReplayMode?: boolean;
|
||||
isGenerating?: boolean;
|
||||
currentStep?: number;
|
||||
ScrollToBottomButtonRef?: any;
|
||||
currentConversationId?: string;
|
||||
startTimer: () => void;
|
||||
}
|
||||
|
||||
export const MessageList: React.FC<MessageListProps> = ({
|
||||
messages = [],
|
||||
onClarificationSelect,
|
||||
onAddMessage,
|
||||
isThinking = false,
|
||||
isReplayMode = false,
|
||||
isGenerating = false,
|
||||
currentStep,
|
||||
currentConversationId,
|
||||
ScrollToBottomButtonRef = useRef<any>(),
|
||||
startTimer = () => {},
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [displayedMessages, setDisplayedMessages] = useState<MessageType[]>([]);
|
||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
|
||||
const [toBottomBtn, setToBottomBtn] = useState(true);
|
||||
const messageListRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToBottom = () => {
|
||||
// console.log(
|
||||
// ScrollToBottomButtonRef.current,
|
||||
// "ScrollToBottomButtonRef.current"
|
||||
// );
|
||||
if (ScrollToBottomButtonRef.current) {
|
||||
// console.log("--------");
|
||||
ScrollToBottomButtonRef.current.scrollToBottom("auto");
|
||||
}
|
||||
// messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// Modify message processing logic, add conversation ID filtering
|
||||
const processedMessages = useMemo(() => {
|
||||
if (!Array.isArray(messages)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// First filter out messages belonging to current conversation
|
||||
// const currentConversationMessages = messages.filter(msg =>
|
||||
// !currentConversationId || msg.conversation_id === currentConversationId
|
||||
// );
|
||||
// Sort by time
|
||||
const sortedMessages = [...messages].sort(
|
||||
(a, b) =>
|
||||
new Date(a.create_time).getTime() - new Date(b.create_time).getTime(),
|
||||
);
|
||||
|
||||
// Merge consecutive running messages
|
||||
return sortedMessages.reduce((acc: MessageType[], curr, index) => {
|
||||
// Add directly if it's the first message
|
||||
if (index === 0) {
|
||||
return [curr];
|
||||
}
|
||||
|
||||
const prevMessage = acc[acc.length - 1];
|
||||
const isSameConversation =
|
||||
curr.conversation_id === prevMessage.conversation_id;
|
||||
const isPrevRunning = prevMessage.status === MessageState.RUNNING;
|
||||
const isCurrRunning = curr.status === MessageState.RUNNING;
|
||||
const isSameType = curr.type === prevMessage.type;
|
||||
|
||||
// If previous message is running state, and current message is also running or finished state
|
||||
// and is the same type of message from the same conversation, update the previous message
|
||||
if (
|
||||
isSameConversation &&
|
||||
isPrevRunning &&
|
||||
isSameType &&
|
||||
(isCurrRunning || curr.status === MessageState.FINISHED)
|
||||
) {
|
||||
acc[acc.length - 1] = {
|
||||
...curr,
|
||||
id: prevMessage.id, // Keep original id to maintain React key stability
|
||||
};
|
||||
} else {
|
||||
acc.push(curr);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}, [messages, currentConversationId]);
|
||||
|
||||
// Message streaming output in replay mode
|
||||
useEffect(() => {
|
||||
// console.log(messages,"processedMessages")
|
||||
|
||||
if (!isReplayMode) {
|
||||
setDisplayedMessages(processedMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentMessageIndex < processedMessages.length) {
|
||||
const currentMessage = processedMessages[currentMessageIndex];
|
||||
const messageWithStreaming = {
|
||||
...currentMessage,
|
||||
status: MessageState.RUNNING,
|
||||
};
|
||||
|
||||
setDisplayedMessages((prev) => [...prev, messageWithStreaming]);
|
||||
|
||||
// Simulate streaming output
|
||||
const timer = setTimeout(() => {
|
||||
setDisplayedMessages((prev) => {
|
||||
const newMessages = [...prev];
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...currentMessage,
|
||||
status: MessageState.FINISHED,
|
||||
};
|
||||
return newMessages;
|
||||
});
|
||||
setCurrentMessageIndex((prev) => prev + 1);
|
||||
}, 500); // Display each message for 0.5 seconds
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isReplayMode, currentMessageIndex, processedMessages]);
|
||||
|
||||
// Reset state when message list changes
|
||||
useEffect(() => {
|
||||
if (isReplayMode) {
|
||||
setCurrentMessageIndex(0);
|
||||
setDisplayedMessages([]);
|
||||
} else {
|
||||
setDisplayedMessages(processedMessages);
|
||||
}
|
||||
}, [messages, isReplayMode, processedMessages]);
|
||||
|
||||
// Update displayed messages when currentStep changes
|
||||
useEffect(() => {
|
||||
if (currentStep !== undefined) {
|
||||
const messagesToShow = processedMessages.slice(0, currentStep);
|
||||
setDisplayedMessages(messagesToShow);
|
||||
setCurrentMessageIndex(currentStep);
|
||||
}
|
||||
}, [currentStep, processedMessages]);
|
||||
const shouldScrollRef = useRef(isAtBottom.current);
|
||||
|
||||
useEffect(() => {
|
||||
// Sync latest scroll condition to ref
|
||||
shouldScrollRef.current = isAtBottom.current;
|
||||
}, [isAtBottom.current]);
|
||||
useLayoutEffect(() => {
|
||||
// Exit directly if scrolling is not needed
|
||||
if (!shouldScrollRef.current) return;
|
||||
startTimer();
|
||||
}, [displayedMessages, isThinking, startTimer]);
|
||||
useEffect(() => {
|
||||
if (toBottomBtn && displayedMessages.length > 0) {
|
||||
scrollToBottom();
|
||||
setToBottomBtn(false);
|
||||
}
|
||||
}, [displayedMessages, isThinking]);
|
||||
return (
|
||||
<div className={styles.messageList} ref={messageListRef}>
|
||||
{displayedMessages.map((message) => (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
messages={displayedMessages}
|
||||
onClarificationSelect={onClarificationSelect}
|
||||
isReplayMode={isReplayMode}
|
||||
isGenerating={isGenerating}
|
||||
/>
|
||||
))}
|
||||
{isThinking && <ThinkingMessage />}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageList);
|
||||
26
alias/frontend/src/components/Chat/ResponseMessage.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { ResponseMessage as ResponseMessageType } from "@/types/message";
|
||||
import { BaseMessage } from "./BaseMessage";
|
||||
import styles from "./Message.module.scss";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface ResponseMessageProps {
|
||||
message: ResponseMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const ResponseMessage: React.FC<ResponseMessageProps> = ({
|
||||
message,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
};
|
||||
23
alias/frontend/src/components/Chat/SubResponseMessage.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { SubResponseMessage as SubResponseMessageType } from "@/types/message";
|
||||
import { BaseMessage } from "./BaseMessage";
|
||||
import styles from "./Message.module.scss";
|
||||
|
||||
interface SubResponseMessageProps {
|
||||
message: SubResponseMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const SubResponseMessage: React.FC<SubResponseMessageProps> = ({
|
||||
message,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div className={styles.subResponseMessage}>
|
||||
{/* <img src={iconCheckbox} alt="checkbox" className={styles.checkbox} /> */}
|
||||
<div className={styles.subResponseContent}>{message.content}</div>
|
||||
</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
};
|
||||
22
alias/frontend/src/components/Chat/SubThoughtMessage.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { SubThoughtMessage as SubThoughtMessageType } from "@/types/message";
|
||||
import { BaseMessage } from "./BaseMessage";
|
||||
import styles from "./Message.module.scss";
|
||||
|
||||
interface SubThoughtMessageProps {
|
||||
message: SubThoughtMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const SubThoughtMessage: React.FC<SubThoughtMessageProps> = ({
|
||||
message,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div className={styles.subThoughtMessage}>
|
||||
<div className={styles.subThoughtContent}>{message.content}</div>
|
||||
</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
};
|
||||
19
alias/frontend/src/components/Chat/SystemMessage.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { SystemMessage as SystemMessageType } from "@/types/message";
|
||||
import { BaseMessage } from "./BaseMessage";
|
||||
|
||||
interface SystemMessageProps {
|
||||
message: SystemMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const SystemMessage: React.FC<SystemMessageProps> = ({
|
||||
message,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<BaseMessage message={message} onFeedback={onFeedback}>
|
||||
<div>{message.content}</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
};
|
||||
20
alias/frontend/src/components/Chat/ThinkingMessage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import styles from "./Message.module.scss";
|
||||
import AssistantAvatar from "@/assets/icons/avatar/assistantHeader.png";
|
||||
import { Flex } from "antd";
|
||||
|
||||
export const ThinkingMessage: React.FC = () => {
|
||||
return (
|
||||
<Flex gap="middle" align="center">
|
||||
<div className={styles.avatar}>
|
||||
<img src={AssistantAvatar} alt="Agent" />
|
||||
</div>
|
||||
<div className={styles.thinkingText}>Agent is thinking</div>
|
||||
<div className={styles.thinkingDots}>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
16
alias/frontend/src/components/Chat/ThoughtMessage.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ThoughtMessage as ThoughtMessageType } from "@/types/message";
|
||||
|
||||
interface ThoughtMessageProps {
|
||||
message: ThoughtMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const ThoughtMessage: React.FC<ThoughtMessageProps> = ({ message }) => {
|
||||
return (
|
||||
<div className="flex items-start gap-2 p-2 bg-gray-50 rounded">
|
||||
<span className="text-lg">💭</span>
|
||||
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
.toolCallMessage {
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
border-radius: 8px;
|
||||
.toolCallHeader {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.arguments {
|
||||
border-left: solid 1px var(--sps-color-border-secondary);
|
||||
margin: 0px 12px 12px 20px;
|
||||
}
|
||||
.contents {
|
||||
white-space: pre-wrap;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
121
alias/frontend/src/components/Chat/ToolCallMessage/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useWorkspace } from "@/context/WorkspaceContext.tsx";
|
||||
import type { ToolCallMessage as ToolCallMessageType } from "@/types/message";
|
||||
import {
|
||||
SparkBrowseLine,
|
||||
SparkDownLine,
|
||||
SparkLocalFileLine,
|
||||
SparkToolLine,
|
||||
SparkUpLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import { Flex } from "antd";
|
||||
import React, { memo, useEffect, useState } from "react";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface ToolCallMessageProps {
|
||||
message: ToolCallMessageType;
|
||||
onFeedback?: (messageId: string, feedback: "like" | "dislike" | null) => void;
|
||||
}
|
||||
|
||||
export const ToolCallMessage: React.FC<ToolCallMessageProps> = memo(
|
||||
({ message }) => {
|
||||
const { setDisplayedContent, setActiveKey, setArgs, setMessageList } =
|
||||
useWorkspace();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
useEffect(() => {
|
||||
setDisplayedContent(message.content);
|
||||
setArgs(message?.arguments);
|
||||
setMessageList((prev) => {
|
||||
// Filter out old messages with same id
|
||||
const filtered = prev.filter(
|
||||
(m) => m.id !== message.id && m.content !== message.content,
|
||||
);
|
||||
// Add new message
|
||||
return [...filtered, message];
|
||||
});
|
||||
}, [message]); // Depend on entire message object
|
||||
|
||||
const getIcon = () => {
|
||||
// Determine icon based on tool name if icon is not specified
|
||||
if (!message.icon) {
|
||||
const toolName =
|
||||
("tool_name" in message ? message.tool_name : undefined) ||
|
||||
message.name ||
|
||||
"";
|
||||
if (toolName.toLowerCase().includes("browser")) {
|
||||
return <SparkBrowseLine />;
|
||||
}
|
||||
if (toolName.toLowerCase().includes("file")) {
|
||||
return <SparkLocalFileLine />;
|
||||
}
|
||||
} else {
|
||||
switch (message.icon) {
|
||||
case "browser":
|
||||
return <SparkBrowseLine />;
|
||||
case "file":
|
||||
return <SparkLocalFileLine />;
|
||||
default:
|
||||
return <SparkToolLine />;
|
||||
}
|
||||
}
|
||||
return <SparkToolLine />;
|
||||
};
|
||||
|
||||
const getToolName = () => {
|
||||
if (message.type === "tool_use")
|
||||
return `Using Tool: ${message?.tool_name || message.name}`;
|
||||
if (message.type === "tool_result")
|
||||
return `Tool result: ${message?.tool_name || message.name}`;
|
||||
return message?.tool_name || message.name;
|
||||
};
|
||||
|
||||
const getToolArguments = () => {
|
||||
const argContents = JSON.stringify(message.arguments, null, 2);
|
||||
if (!argContents || argContents === "{}" || argContents === "{ }") {
|
||||
try {
|
||||
const content = JSON.parse(message.content);
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
const output = content[0]?.output;
|
||||
if (typeof output === "string") {
|
||||
return output;
|
||||
}
|
||||
if (Array.isArray(output) && output.length > 0) {
|
||||
return JSON.stringify(output?.[0], null, 2);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Return original content or empty string if parsing fails
|
||||
console.error("Failed to parse message content:", error);
|
||||
return message.content || "";
|
||||
}
|
||||
}
|
||||
return argContents;
|
||||
};
|
||||
return (
|
||||
<Flex vertical className={styles.toolCallMessage}>
|
||||
<Flex
|
||||
gap="small"
|
||||
justify="space-between"
|
||||
className={styles.toolCallHeader}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
setDisplayedContent(message.content);
|
||||
setArgs(message.arguments);
|
||||
setActiveKey("1");
|
||||
}}
|
||||
>
|
||||
<Flex gap="small">
|
||||
{getIcon()}
|
||||
{getToolName()}
|
||||
</Flex>
|
||||
{!isExpanded ? <SparkDownLine /> : <SparkUpLine />}
|
||||
</Flex>
|
||||
{isExpanded && message.arguments && (
|
||||
<div className={styles.arguments}>
|
||||
<div className={styles.contents}>{getToolArguments()}</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
.userMessageFlex {
|
||||
margin-bottom: 24px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.userMessageText {
|
||||
width: 100%;
|
||||
}
|
||||
.userContentFlex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--sps-color-primary-bg);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.roadmapCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 6px 0 rgba(99, 102, 241, 0.06);
|
||||
padding: 10px 18px;
|
||||
min-width: 140px;
|
||||
max-width: 260px;
|
||||
margin-bottom: 0;
|
||||
background-color: var(--sps-color-bg-base);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--sps-color-border-secondary);
|
||||
&:hover,
|
||||
&.selected {
|
||||
box-shadow: 0 2px 12px 0 rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.roadmapLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
}
|
||||
|
||||
.roadmapRight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.description {
|
||||
color: var(--sps-color-text-quaternary);
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: var(--sps-color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.diffRoadmap {
|
||||
height: 600px;
|
||||
border-top: 1px solid var(--sps-color-border-secondary);
|
||||
.left {
|
||||
border-right: 1px solid var(--sps-color-border-secondary);
|
||||
}
|
||||
.diffRoadmapJson {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px 18px;
|
||||
}
|
||||
}
|
||||
107
alias/frontend/src/components/Chat/UserMessage/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { UserMessage as UserMessageType } from "@/types/message";
|
||||
import { Modal } from "@agentscope-ai/design";
|
||||
import { SparkProcessJudgmentLine } from "@agentscope-ai/icons";
|
||||
import { Flex } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark, prism } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { FileItems } from "../FileItems";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface UserMessageProps {
|
||||
message: UserMessageType;
|
||||
}
|
||||
|
||||
export const UserMessage: React.FC<UserMessageProps> = ({ message }) => {
|
||||
const { files, roadmap } = message;
|
||||
const { theme } = useTheme();
|
||||
const [diffOpen, setDiffOpen] = useState(false);
|
||||
const fontSize = { fontSize: 20 };
|
||||
const viewRoadmapDiff = () => {
|
||||
setDiffOpen(true);
|
||||
};
|
||||
const onCancel = () => {
|
||||
setDiffOpen(false);
|
||||
};
|
||||
const customStyle = {
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
overflow: "auto",
|
||||
padding: 0,
|
||||
backgroundColor: "var(--sps-color-bg-base)",
|
||||
};
|
||||
const CodeView = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language="JSON"
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
style={theme === "dark" ? oneDark : prism}
|
||||
customStyle={
|
||||
theme === "dark"
|
||||
? customStyle
|
||||
: { ...customStyle, background: "transparent" }
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Flex gap="small" vertical className={styles.userMessageFlex}>
|
||||
<Flex justify="flex-end" className={styles.userMessageText}>
|
||||
<div className={styles.userContentFlex}>{message.content}</div>
|
||||
</Flex>
|
||||
{files && files?.length > 0 && (
|
||||
<Flex wrap gap="small" justify="flex-end">
|
||||
<FileItems files={files} />
|
||||
</Flex>
|
||||
)}
|
||||
{roadmap && (
|
||||
<Flex justify="flex-end">
|
||||
<div className={styles.roadmapCard} onClick={viewRoadmapDiff}>
|
||||
<SparkProcessJudgmentLine
|
||||
className={styles.roadmapLeft}
|
||||
style={fontSize}
|
||||
/>
|
||||
<div className={styles.roadmapRight}>
|
||||
<div className={styles.title}>Roadmap</div>
|
||||
<div className={styles.description}>{`${
|
||||
roadmap.current?.subtasks?.length || 0
|
||||
} Tasks`}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
{diffOpen && roadmap && (
|
||||
<Modal
|
||||
width={960}
|
||||
open={diffOpen}
|
||||
showDivider={false}
|
||||
title="Roadmap Diff"
|
||||
footer={null}
|
||||
onCancel={onCancel}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<Flex className={styles.diffRoadmap}>
|
||||
<div className={`${styles.left} ${styles.diffRoadmapJson}`}>
|
||||
<div>Previous JSON</div>
|
||||
{roadmap.previous && (
|
||||
<CodeView value={JSON.stringify(roadmap.previous, null, 2)} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.diffRoadmapJson}>
|
||||
<div>Current JSON</div>
|
||||
{roadmap.current && (
|
||||
<CodeView value={JSON.stringify(roadmap.current, null, 2)} />
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
34
alias/frontend/src/components/LoginModal/index.module.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.modalWrap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
h1 {
|
||||
color: var(--sps-color-text);
|
||||
font-weight: 600;
|
||||
font-size: 2rem;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
text-align: center;
|
||||
}
|
||||
.tips {
|
||||
width: 336px;
|
||||
text-align: center;
|
||||
color: var(--sps-color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.logBtn {
|
||||
height: 2.5rem;
|
||||
width: 336px;
|
||||
margin-top: 30px;
|
||||
font-size: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
.registerBtn {
|
||||
height: 2.5rem;
|
||||
width: 336px;
|
||||
margin-top: 10px;
|
||||
font-size: 100%;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
63
alias/frontend/src/components/LoginModal/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Button, Modal } from "@agentscope-ai/design";
|
||||
import { memo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const LoginModal = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(true);
|
||||
const navigate = useNavigate();
|
||||
const showModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
if (
|
||||
localStorage.getItem("access_token") === null &&
|
||||
localStorage.getItem("refresh_token") === null
|
||||
)
|
||||
return (
|
||||
<Modal
|
||||
// title="Basic Modal"
|
||||
open={isModalOpen}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
centered={true}
|
||||
width={384}
|
||||
closable={false}
|
||||
>
|
||||
<div className={styles.modalWrap}>
|
||||
<h1>Welcome</h1>
|
||||
<p className={styles.tips}>
|
||||
Login or register to chat with AgentScope, upload files and images,
|
||||
generate images or videos, etc.
|
||||
</p>
|
||||
<Button
|
||||
type="primary"
|
||||
className={styles.logBtn}
|
||||
onClick={() => {
|
||||
navigate("/login?mode=login");
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.registerBtn}
|
||||
onClick={() => {
|
||||
navigate("/login?mode=register");
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
export default memo(LoginModal);
|
||||
32
alias/frontend/src/components/LogoIcon/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { GetProps } from "antd";
|
||||
import Icon from "@ant-design/icons";
|
||||
|
||||
type CustomIconComponentProps = GetProps<typeof Icon>;
|
||||
const LogoIconSvg = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
width="77"
|
||||
height="48"
|
||||
viewBox="0 0 77 48"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M37.5018,17.8196C36.7176,14.3736,33.9865,11.68249,30.4893,10.90978C33.9865,10.13707,36.7176,7.44597,37.5018,4C38.286,7.44597,41.0171,10.13707,44.5143,10.90978C41.0171,11.68249,38.286,14.3736,37.5018,17.8196ZM9.877,13.9301L0,35.7632L5.26642,35.7632L7.23737,31.0924L17.5179,31.0924L19.4889,35.7632L24.8438,35.7632L14.9481,13.9301L9.877,13.9301ZM15.8999,27.258L12.3777,18.911099999999998L8.85542,27.258L15.8999,27.258ZM26.199,14.4227L26.199,35.7632L31.137,35.7632L31.137,14.4227L26.199,14.4227ZM54.4086,33.6971L54.4086,35.7632L59.0503,35.7632L59.0503,26.179Q59.0503,22.3578,56.9028,20.5531Q54.7553,18.747999999999998,50.846,18.747999999999998Q48.8202,18.747999999999998,46.8616,19.266Q44.903,19.7836,43.4984,20.7758L45.2616,24.1619Q46.1929,23.444,47.51,23.0293Q48.8276,22.6141,50.1709,22.6141Q52.179,22.6141,53.1454,23.4762Q54.1123,24.3378,54.1123,25.9056L54.1123,25.9085L50.1877,25.9085Q47.5926,25.9085,45.9788,26.5484Q44.3654,27.1878,43.6255,28.3141Q42.8856,29.4398,42.8856,30.9233Q42.8856,32.3703,43.6423,33.5243Q44.399,34.677800000000005,45.8234,35.339200000000005Q47.2479,36,49.2346,36Q51.4821,36,52.907,35.1584Q53.858,34.5968,54.4086,33.6971ZM54.1123,30.456L54.1123,28.7439L50.7352,28.7439Q48.9844,28.7439,48.338,29.3005Q47.6915,29.8565,47.6915,30.7245Q47.6915,31.6368,48.4275,32.187Q49.1634,32.7367,50.4513,32.7367Q51.6952,32.7367,52.6849,32.168Q53.6746,31.5993,54.1123,30.456ZM64.4221,35.484899999999996Q66.4316,36,68.59,36Q71.1579,36,72.9513,35.3177Q74.7452,34.6349,75.6884,33.4317Q76.6316,32.2284,76.6316,30.6811Q76.6316,29.2532,76.0742,28.3584Q75.5168,27.4632,74.6018,26.949Q73.6868,26.4344,72.5883,26.1571Q71.4898,25.8798,70.3913,25.7302Q69.2928,25.5801,68.3778,25.4017Q67.4628,25.2233,66.9054,24.9046Q66.348,24.5859,66.348,23.967Q66.348,23.2974,67.1062,22.8597Q67.8644,22.4221,69.5312,22.4221Q70.6965,22.4221,71.9399,22.6872Q73.1838,22.9523,74.4178,23.6697L76.0747,20.1891Q74.8664,19.4907,73.072,19.119300000000003Q71.2781,18.747999999999998,69.5124,18.747999999999998Q67.0395,18.747999999999998,65.2807,19.439999999999998Q63.5224,20.1316,62.5822,21.3475Q61.6425,22.5634,61.6425,24.1478Q61.6425,25.5947,62.1999,26.4992Q62.7573,27.4032,63.6718,27.9271Q64.5868,28.4505,65.6917,28.722Q66.7966,28.9934,67.8951,29.1431Q68.9941,29.2927,69.9091,29.4525Q70.8241,29.6119,71.381,29.915Q71.9384,30.2181,71.9384,30.7995Q71.9384,31.5125,71.2212,31.9195Q70.5046,32.325900000000004,68.8056,32.325900000000004Q67.2353,32.325900000000004,65.6338,31.8829Q64.0323,31.4394,62.8745,30.7221L61.2176,34.2027Q62.4131,34.9698,64.4221,35.484899999999996ZM35.0938,20.7869L35.0938,35.7632L40.0318,35.7632L40.0318,20.7869L35.0938,20.7869Z"
|
||||
fillRule="evenodd"
|
||||
fill="currentColor"
|
||||
fillOpacity="1"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
const LogoIcon = (props: Partial<CustomIconComponentProps>) => (
|
||||
<Icon component={LogoIconSvg} {...props} />
|
||||
);
|
||||
|
||||
export default LogoIcon;
|
||||
28
alias/frontend/src/components/Roadmap/index.module.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.roadmap {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 123px);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-top: solid 1px var(--sps-color-border-secondary);
|
||||
padding: 16px 20px;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
.roadmapList {
|
||||
border-radius: 8px;
|
||||
border: solid 1px var(--sps-color-border-secondary);
|
||||
margin-top: 16px;
|
||||
}
|
||||
.listItem {
|
||||
padding: 12px;
|
||||
}
|
||||
.itemFlex {
|
||||
width: 100%;
|
||||
}
|
||||
.success {
|
||||
color: var(--sps-color-text-tertiary);
|
||||
}
|
||||
}
|
||||
375
alias/frontend/src/components/Roadmap/index.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
useMemo,
|
||||
createContext,
|
||||
memo,
|
||||
} from "react";
|
||||
import diff from "deep-diff";
|
||||
import {
|
||||
Button,
|
||||
message,
|
||||
Input,
|
||||
AlertDialog,
|
||||
IconButton,
|
||||
Modal,
|
||||
} from "@agentscope-ai/design";
|
||||
import { List, Flex, GetProps } from "antd";
|
||||
import {
|
||||
SparkPlusLine,
|
||||
SparkSaveLine,
|
||||
SparkDeleteLine,
|
||||
SparkEditLine,
|
||||
SparkDragDotLine,
|
||||
SparkIncompleteLine,
|
||||
SparkLoadingLine,
|
||||
SparkCheckCircleLine,
|
||||
SparkProcessFailedLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import classNames from "classnames";
|
||||
import styles from "./index.module.scss";
|
||||
import { SubtasksProps, RoadMapDataProps, RoadMapType } from "@/types/roadmap";
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import type { DragEndEvent, DraggableAttributes } from "@dnd-kit/core";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
interface RoadmapProps {
|
||||
data?: RoadMapDataProps | null;
|
||||
conversationId: string;
|
||||
editable?: boolean;
|
||||
onSave?: (data: RoadMapDataProps) => void;
|
||||
}
|
||||
|
||||
interface SortableListItemContextProps {
|
||||
setActivatorNodeRef?: (element: HTMLElement | null) => void;
|
||||
listeners?: SyntheticListenerMap;
|
||||
attributes?: DraggableAttributes;
|
||||
}
|
||||
|
||||
const fontSize = { fontSize: 20 };
|
||||
const SortableListItemContext = createContext<SortableListItemContextProps>({});
|
||||
const DragHandle: React.FC = () => {
|
||||
const { setActivatorNodeRef, listeners, attributes } = useContext(
|
||||
SortableListItemContext,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<SparkDragDotLine />}
|
||||
style={{ cursor: "move" }}
|
||||
ref={setActivatorNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SortableListItem: React.FC<
|
||||
GetProps<typeof List.Item> & { itemKey: number }
|
||||
> = (props) => {
|
||||
const { itemKey, style, ...rest } = props;
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: itemKey });
|
||||
|
||||
const listStyle: React.CSSProperties = {
|
||||
...style,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
...(isDragging ? { position: "relative", zIndex: 9999 } : {}),
|
||||
};
|
||||
|
||||
const memoizedValue = useMemo<SortableListItemContextProps>(
|
||||
() => ({ setActivatorNodeRef, listeners, attributes }),
|
||||
[setActivatorNodeRef, listeners, attributes],
|
||||
);
|
||||
|
||||
return (
|
||||
<SortableListItemContext.Provider value={memoizedValue}>
|
||||
<List.Item {...rest} ref={setNodeRef} style={listStyle} />
|
||||
</SortableListItemContext.Provider>
|
||||
);
|
||||
};
|
||||
const ReadListItem: React.FC<
|
||||
GetProps<typeof List.Item> & { item: SubtasksProps }
|
||||
> = ({ item }) => {
|
||||
const { state, description } = item || {};
|
||||
return (
|
||||
<List.Item className={styles.listItem}>
|
||||
<Flex
|
||||
gap="small"
|
||||
align="center"
|
||||
className={classNames(styles.itemFlex, {
|
||||
[styles.success]: state === RoadMapType.DONE,
|
||||
})}
|
||||
>
|
||||
<Flex gap="small">
|
||||
{state === RoadMapType.IN_PROGRESS && (
|
||||
<SparkLoadingLine style={{ ...fontSize, color: "#0B83F1" }} />
|
||||
)}
|
||||
{state === RoadMapType.DONE && (
|
||||
<SparkCheckCircleLine style={fontSize} />
|
||||
)}
|
||||
{state === RoadMapType.TODO && (
|
||||
<SparkIncompleteLine style={fontSize} />
|
||||
)}
|
||||
{state === RoadMapType.ABANDONED && (
|
||||
<SparkProcessFailedLine style={fontSize} />
|
||||
)}
|
||||
{description}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const Roadmap: React.FC<RoadmapProps> = ({
|
||||
data,
|
||||
conversationId,
|
||||
editable,
|
||||
onSave = (data: RoadMapDataProps) => {},
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [taskValue, setTaskValue] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [list, setList] = useState<SubtasksProps[]>(data?.subtasks || []);
|
||||
const [taskKey, setTaskKey] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.subtasks && Array.isArray(data?.subtasks)) {
|
||||
const newList = data.subtasks.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
key: index + 1,
|
||||
};
|
||||
});
|
||||
setList(newList);
|
||||
}
|
||||
}, [data?.subtasks]);
|
||||
|
||||
const onDragEnd = ({ active, over }: DragEndEvent) => {
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
if (active.id !== over.id) {
|
||||
setList((prevState) => {
|
||||
const activeIndex = prevState.findIndex((i) => i.key === active.id);
|
||||
const overIndex = prevState.findIndex((i) => i.key === over.id);
|
||||
return arrayMove(prevState, activeIndex, overIndex);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveHandle = () => {
|
||||
AlertDialog.info({
|
||||
title: "Confirm save?",
|
||||
children:
|
||||
"The result you edited will overwrite the original data and become the new roadmap and start execution.",
|
||||
centered: true,
|
||||
okText: "Save",
|
||||
onOk: async () => {
|
||||
const newList = list.map(({ key, ...rest }) => rest);
|
||||
const newData = { subtasks: newList };
|
||||
const differences: any = diff(data, newData);
|
||||
if (differences) {
|
||||
try {
|
||||
const response: any = await conversationApi.setRoadmap(
|
||||
conversationId,
|
||||
newData,
|
||||
);
|
||||
if (response.status && response?.payload) {
|
||||
onSave(response?.payload);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Failed to update roadmap");
|
||||
console.error("Error updating roadmap:", error);
|
||||
}
|
||||
} else {
|
||||
message.info("No changes detected, nothing to update.");
|
||||
setIsEditing(false);
|
||||
}
|
||||
onCancel();
|
||||
},
|
||||
});
|
||||
};
|
||||
const onAddTask = () => {
|
||||
setOpen(true);
|
||||
setTaskValue("");
|
||||
setTaskKey(undefined);
|
||||
};
|
||||
const deletedHandle = (key: number) => {
|
||||
AlertDialog.warning({
|
||||
title: "Confirm deletion of this task?",
|
||||
children:
|
||||
"Once deleted, it cannot be recovered. Please proceed with caution.",
|
||||
centered: true,
|
||||
okText: "Confirm deletion",
|
||||
onOk: () => {
|
||||
setList(list.filter((item) => item.key !== key));
|
||||
},
|
||||
});
|
||||
};
|
||||
const updateTaskHandle = (content: string, key: number) => {
|
||||
setOpen(true);
|
||||
setTaskValue(content);
|
||||
setTaskKey(key);
|
||||
};
|
||||
const onCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const onOk = () => {
|
||||
const trimmedValue = taskValue.trim();
|
||||
if (!trimmedValue) {
|
||||
message.info("Please enter the task");
|
||||
return;
|
||||
}
|
||||
if (taskKey) {
|
||||
setList(
|
||||
list.map((item) =>
|
||||
item.key === taskKey ? { ...item, description: taskValue } : item,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setList([
|
||||
...list,
|
||||
{
|
||||
key: new Date().getTime(),
|
||||
state: RoadMapType.TODO,
|
||||
description: taskValue.trim(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
onCancel();
|
||||
};
|
||||
// const renderReadItem =
|
||||
return (
|
||||
<div className={styles.roadmap}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<div className={styles.title}>Roadmap</div>
|
||||
{editable && isEditing && (
|
||||
<Flex gap="small">
|
||||
<Button onClick={onAddTask}>
|
||||
<SparkPlusLine /> Add Task
|
||||
</Button>
|
||||
<Button type="primary" onClick={onSaveHandle}>
|
||||
<SparkSaveLine /> Save
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
{editable && !isEditing && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>
|
||||
<SparkEditLine /> Edit
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<DndContext
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={onDragEnd}
|
||||
id="list-drag-sorting-handler"
|
||||
>
|
||||
<SortableContext
|
||||
items={list.map((item, index) => item.key || index)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<List
|
||||
className={styles.roadmapList}
|
||||
dataSource={list}
|
||||
renderItem={(item, index) => {
|
||||
const { description, state, key } = item || {};
|
||||
if (isEditing && editable)
|
||||
return (
|
||||
<SortableListItem
|
||||
key={key}
|
||||
itemKey={key || index}
|
||||
className={styles.listItem}
|
||||
>
|
||||
<Flex
|
||||
gap="small"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
className={styles.itemFlex}
|
||||
>
|
||||
<Flex gap="small">
|
||||
<DragHandle />
|
||||
{state === RoadMapType.IN_PROGRESS && (
|
||||
<SparkLoadingLine
|
||||
style={{ ...fontSize, color: "#0B83F1" }}
|
||||
/>
|
||||
)}
|
||||
{state === RoadMapType.DONE && (
|
||||
<SparkCheckCircleLine style={fontSize} />
|
||||
)}
|
||||
{state === RoadMapType.TODO && (
|
||||
<SparkIncompleteLine style={fontSize} />
|
||||
)}
|
||||
{state === RoadMapType.ABANDONED && (
|
||||
<SparkProcessFailedLine style={fontSize} />
|
||||
)}
|
||||
{description}
|
||||
</Flex>
|
||||
<Flex gap="small">
|
||||
{item.state === RoadMapType.TODO && (
|
||||
<IconButton
|
||||
icon={<SparkEditLine style={fontSize} />}
|
||||
bordered={false}
|
||||
onClick={() => {
|
||||
updateTaskHandle(description, key || index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<SparkDeleteLine style={fontSize} />}
|
||||
bordered={false}
|
||||
onClick={() => {
|
||||
deletedHandle(key || index);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</SortableListItem>
|
||||
);
|
||||
return <ReadListItem item={item} />;
|
||||
}}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
okText="Sure"
|
||||
title="Edit Task"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={Math.min(Math.max(3, taskValue.split("\n").length + 1), 20)}
|
||||
onChange={(v) => {
|
||||
setTaskValue(v.target.value || "");
|
||||
}}
|
||||
value={taskValue}
|
||||
autoSize={{ minRows: 10, maxRows: 20 }}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(Roadmap);
|
||||
22
alias/frontend/src/components/SandBox/index.module.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.sandbox {
|
||||
width: 100%;
|
||||
height: calc(100vh - 134px);
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
}
|
||||
.sandboxIframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
33
alias/frontend/src/components/SandBox/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { useEffect, memo } from "react";
|
||||
import { Result } from "@agentscope-ai/design";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface SandBoxProps {
|
||||
sandboxUrl: string;
|
||||
}
|
||||
|
||||
const SandBox: React.FC<SandBoxProps> = ({ sandboxUrl }) => {
|
||||
return (
|
||||
<div className={styles.sandbox}>
|
||||
{/* <div className={styles.title}>{sandboxUrl}</div> */}
|
||||
{sandboxUrl && (
|
||||
<iframe
|
||||
src={sandboxUrl}
|
||||
className={styles.sandboxIframe}
|
||||
title="Sandbox"
|
||||
allowFullScreen
|
||||
frameBorder="0"
|
||||
/>
|
||||
)}
|
||||
{!sandboxUrl && (
|
||||
<Result
|
||||
type="error"
|
||||
title="Error"
|
||||
description="Please try again later"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SandBox);
|
||||
@@ -0,0 +1,54 @@
|
||||
// Key stylesheet section
|
||||
.scrollRoot {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// Hide scrollbar but keep scroll functionality
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollButton {
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
bottom: 20px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--scroll-button-bg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
background: var(--scroll-button-bg-hover);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
117
alias/frontend/src/components/ScrollToBottomButton/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { isAtBottom, isManualScrolling } from "@/utils/sharedRefs";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import styles from "./index.module.scss";
|
||||
// import { debounce } from "lodash";
|
||||
interface ScrollToBottomButtonProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
autoScrollThreshold?: number;
|
||||
}
|
||||
interface ScrollToBottomButtonHandles {
|
||||
scrollToBottom: (behavior?: ScrollBehavior) => void;
|
||||
}
|
||||
|
||||
const ScrollToBottomButton = forwardRef<
|
||||
ScrollToBottomButtonHandles,
|
||||
ScrollToBottomButtonProps
|
||||
>(({ children, autoScrollThreshold = 1, className, ...props }, ref) => {
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isScroll = useRef(false);
|
||||
const isAutoScrolling = useRef<boolean>(false);
|
||||
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Precise bottom detection
|
||||
const checkIsAtBottom = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return true;
|
||||
// const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
// Handle floating point precision: round to integer
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
|
||||
// Calculate difference from bottom and take absolute value
|
||||
const diff = scrollHeight - (scrollTop + clientHeight);
|
||||
const isBottom = Math.abs(diff) <= Math.max(30, autoScrollThreshold);
|
||||
|
||||
isAtBottom.current = isBottom;
|
||||
return isAtBottom.current;
|
||||
}, [autoScrollThreshold]);
|
||||
// Scroll method with lock
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
// Clear previous timer
|
||||
if (scrollTimeout.current) {
|
||||
clearTimeout(scrollTimeout.current);
|
||||
}
|
||||
isScroll.current = false;
|
||||
isAutoScrolling.current = true;
|
||||
setShowButton(false);
|
||||
// container.style.scrollBehavior = behavior;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
container.style.scrollBehavior = "auto";
|
||||
isAtBottom.current = true;
|
||||
isManualScrolling.current = true;
|
||||
|
||||
// Set new timer
|
||||
scrollTimeout.current = setTimeout(() => {
|
||||
if (container) {
|
||||
isAutoScrolling.current = false;
|
||||
// isManualScrolling.current = true;
|
||||
}
|
||||
// Clean up timer reference
|
||||
scrollTimeout.current = null;
|
||||
}, 0);
|
||||
}, []);
|
||||
// Optimized scroll handling
|
||||
const handleScroll = useCallback(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (isAutoScrolling.current) return;
|
||||
setShowButton(!checkIsAtBottom());
|
||||
isScroll.current = !checkIsAtBottom();
|
||||
isManualScrolling.current = false;
|
||||
});
|
||||
}, [checkIsAtBottom]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToBottom,
|
||||
checkIsAtBottom,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={containerRef}
|
||||
className={`${styles.scrollRoot} ${className}`}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{children}
|
||||
|
||||
<div
|
||||
className={styles.scrollAnchor}
|
||||
style={{ position: "absolute", bottom: 0 }}
|
||||
/>
|
||||
|
||||
{isScroll.current && showButton && (
|
||||
<button
|
||||
className={styles.scrollButton}
|
||||
onClick={() => scrollToBottom("auto")}
|
||||
style={{
|
||||
position: "sticky",
|
||||
bottom: "20px",
|
||||
float: "right",
|
||||
animation: `${styles.fadeIn} 0.3s forwards`,
|
||||
}}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
export default ScrollToBottomButton;
|
||||
22
alias/frontend/src/components/ShareModal/index.module.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.container {
|
||||
padding: 16px 0;
|
||||
}
|
||||
.share {
|
||||
margin-bottom: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.urlContainer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.urlInput {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--sps-color-text-tertiary);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
89
alias/frontend/src/components/ShareModal/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import { Conversation } from "@/types/api";
|
||||
import { Button, Input, message, Modal, Switch } from "@agentscope-ai/design";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface ShareModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
shareUrl: string;
|
||||
shared: boolean;
|
||||
conversationId: string;
|
||||
setCurrentConversation: (con: Conversation) => void;
|
||||
}
|
||||
|
||||
export const ShareModal: React.FC<ShareModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
shareUrl,
|
||||
shared,
|
||||
conversationId,
|
||||
setCurrentConversation,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isShared, setIsShared] = useState(shared);
|
||||
|
||||
const handleCopy = () => {
|
||||
// navigator.clipboard.writeText(shareUrl);
|
||||
copy(shareUrl);
|
||||
setCopied(true);
|
||||
|
||||
message.success("Share link copied to clipboard");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
onClose();
|
||||
};
|
||||
const onChangeShare = (share: boolean) => {
|
||||
if (!!conversationId) {
|
||||
conversationApi
|
||||
.shareConversations(conversationId, share)
|
||||
.then((res: any) => {
|
||||
if (res?.payload) {
|
||||
setCurrentConversation(res.payload);
|
||||
setIsShared(share);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error("network error");
|
||||
});
|
||||
} else setIsShared(share);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Share This Conversation"
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={400}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div>Are you sure you want to share this conversation?</div>
|
||||
<Switch
|
||||
className={styles.share}
|
||||
checked={isShared}
|
||||
onChange={onChangeShare}
|
||||
label={isShared ? "Opening" : "Closed"}
|
||||
/>
|
||||
<span></span>
|
||||
<div className={styles.urlContainer}>
|
||||
<Input
|
||||
disabled={!isShared}
|
||||
value={shareUrl}
|
||||
readOnly
|
||||
className={styles.urlInput}
|
||||
/>
|
||||
<Button type="primary" disabled={!isShared} onClick={handleCopy}>
|
||||
{copied ? "Copied" : "Copy Link"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className={styles.description}>
|
||||
Copy this link and share it with others, they can view the contents of
|
||||
this conversation.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
193
alias/frontend/src/components/Viewer/CSVViewer.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React from "react";
|
||||
import { BaseViewerProps } from "./types";
|
||||
|
||||
export const CSVViewer: React.FC<BaseViewerProps> = ({ content, style }) => {
|
||||
const parseCSV = (csvContent: string) => {
|
||||
try {
|
||||
// Normalize line breaks
|
||||
const normalizedContent = csvContent
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n");
|
||||
const rows = normalizedContent.split("\n").filter((row) => row.trim());
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new Error("Empty CSV content");
|
||||
}
|
||||
|
||||
// Simple CSV parsing
|
||||
const parseRow = (row: string) => {
|
||||
try {
|
||||
let cells = [];
|
||||
let cell = "";
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const char = row[i];
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (char === "," && !inQuotes) {
|
||||
cells.push(cell);
|
||||
cell = "";
|
||||
} else {
|
||||
cell += char;
|
||||
}
|
||||
}
|
||||
cells.push(cell);
|
||||
return cells.map((c) => c.trim().replace(/^"|"$/g, ""));
|
||||
} catch (error) {
|
||||
console.error("Error parsing CSV row:", error);
|
||||
throw new Error(`Failed to parse row: ${row}`);
|
||||
}
|
||||
};
|
||||
|
||||
const headers = parseRow(rows[0]);
|
||||
if (headers.length === 0) {
|
||||
throw new Error("No headers found in CSV");
|
||||
}
|
||||
|
||||
const data = rows.slice(1).map((row, index) => {
|
||||
try {
|
||||
const parsedRow = parseRow(row);
|
||||
// Ensure each row has the same number of columns as header
|
||||
if (parsedRow.length !== headers.length) {
|
||||
console.warn(
|
||||
`Row ${index + 1} has ${parsedRow.length} columns, expected ${
|
||||
headers.length
|
||||
}`,
|
||||
);
|
||||
// Pad or truncate columns
|
||||
return parsedRow.length > headers.length
|
||||
? parsedRow.slice(0, headers.length)
|
||||
: [
|
||||
...parsedRow,
|
||||
...Array(headers.length - parsedRow.length).fill(""),
|
||||
];
|
||||
}
|
||||
return parsedRow;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing row ${index + 1}:`, error);
|
||||
// Return empty row instead of interrupting entire parsing
|
||||
return Array(headers.length).fill("");
|
||||
}
|
||||
});
|
||||
|
||||
return { headers, data };
|
||||
} catch (error) {
|
||||
console.error("Error parsing CSV:", error);
|
||||
return {
|
||||
headers: [],
|
||||
data: [],
|
||||
error: error instanceof Error ? error.message : "Failed to parse CSV",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const { headers, data, error } = parseCSV(content);
|
||||
|
||||
// Show error message if parsing fails
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#ff4d4f",
|
||||
backgroundColor: "#fff2f0",
|
||||
border: "1px solid #ffccc7",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
Error: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show prompt message if there is no data
|
||||
if (headers.length === 0 || data.length === 0) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#666",
|
||||
backgroundColor: "#fafafa",
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
No valid CSV data found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
overflow: "auto",
|
||||
// maxHeight: '500px'
|
||||
}}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, index) => (
|
||||
<th
|
||||
key={index}
|
||||
style={{
|
||||
padding: "8px",
|
||||
backgroundColor: "#fafafa",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
}}
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
// Component-level error handling
|
||||
console.error("Component error:", error);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#ff4d4f",
|
||||
backgroundColor: "#fff2f0",
|
||||
border: "1px solid #ffccc7",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
An unexpected error occurred while rendering the CSV viewer
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
57
alias/frontend/src/components/Viewer/ChartViewer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BaseViewerProps } from "./types";
|
||||
|
||||
export const ChartViewer: React.FC<BaseViewerProps> = ({ content, style }) => {
|
||||
const [imageUrl, setImageUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
// Try to parse URL in content
|
||||
try {
|
||||
// If content itself is a URL
|
||||
if (content.startsWith("http")) {
|
||||
setImageUrl(content);
|
||||
} else {
|
||||
// If content contains URL (e.g., in JSON)
|
||||
const matches = content.match(/(https?:\/\/[^\s"]+)/g);
|
||||
if (matches && matches.length > 0) {
|
||||
setImageUrl(matches[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing chart URL:", error);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
if (!imageUrl) {
|
||||
return <div>Invalid chart URL</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: "20px",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Chart"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error("Error loading chart image");
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
72
alias/frontend/src/components/Viewer/CodeViewer.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import React from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark, prism } from "react-syntax-highlighter/dist/esm/styles/prism"; // Use more modern theme
|
||||
import { BaseViewerProps } from "./types";
|
||||
|
||||
interface CodeViewerProps extends BaseViewerProps {
|
||||
language: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const CodeViewer: React.FC<CodeViewerProps> = ({
|
||||
content,
|
||||
language,
|
||||
title,
|
||||
style,
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "fit-content", // Changed to adaptive height
|
||||
// backgroundColor: '#282c34', // Match oneDark theme
|
||||
borderRadius: "4px",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderBottom: "1px solid #3e4451",
|
||||
color: "#abb2bf",
|
||||
fontSize: "14px",
|
||||
fontFamily: "monospace",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<span style={{ opacity: 0.7 }}>{language}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ overflow: "auto" }}>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={theme === "dark" ? oneDark : prism}
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
customStyle={{
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
// maxHeight: 500,
|
||||
overflow: "auto",
|
||||
background: theme === "dark" ? "" : "transparent",
|
||||
}}
|
||||
// lineNumberStyle={{
|
||||
// minWidth: '3em',
|
||||
// paddingRight: '1em',
|
||||
// color: '#495162',
|
||||
// textAlign: 'right',
|
||||
// }}
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
118
alias/frontend/src/components/Viewer/DiffViewer.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { BaseViewerProps } from "./types";
|
||||
|
||||
export interface DiffLine {
|
||||
line: string;
|
||||
type: "addition" | "deletion" | "info" | "context";
|
||||
}
|
||||
|
||||
export const DiffViewer: React.FC<BaseViewerProps> = ({ content, style }) => {
|
||||
const [component, setComponent] = useState<ReactNode>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Parse diff content
|
||||
const parseDiff = (content: string): DiffLine[] => {
|
||||
// Remove diff markers and file information
|
||||
const cleanDiffContent = (raw: string) => {
|
||||
return raw
|
||||
.trim() // First clean leading and trailing whitespace
|
||||
.replace(/^```diff\n|\n```$/g, "") // Remove markdown diff markers
|
||||
.replace(/\\n/g, "\n") // Replace escaped newlines
|
||||
.replace(/\\"/g, '"') // Replace escaped quotes
|
||||
.replace(/\n+$/, "") // Remove trailing newlines
|
||||
.trimEnd(); // Finally clean trailing whitespace
|
||||
};
|
||||
const diffContent = cleanDiffContent(content);
|
||||
|
||||
// show all lines
|
||||
const lines = diffContent.split("\n").filter(
|
||||
(line) => true,
|
||||
// !line.startsWith('Index:') &&
|
||||
// !line.startsWith('===')
|
||||
// !line.startsWith('---') &&
|
||||
// !line.startsWith('+++')
|
||||
);
|
||||
|
||||
return lines.map((line) => {
|
||||
const type = line.startsWith("+")
|
||||
? "addition"
|
||||
: line.startsWith("-")
|
||||
? "deletion"
|
||||
: line.startsWith("@")
|
||||
? "info"
|
||||
: "context";
|
||||
|
||||
return { line, type };
|
||||
});
|
||||
};
|
||||
|
||||
const diffLines = parseDiff(content);
|
||||
|
||||
setComponent(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
// backgroundColor: '#282c34',
|
||||
color: "#abb2bf",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{diffLines.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
backgroundColor:
|
||||
item.type === "addition"
|
||||
? "rgba(40, 167, 69, 0.2)"
|
||||
: item.type === "deletion"
|
||||
? "rgba(203, 36, 49, 0.2)"
|
||||
: item.type === "info"
|
||||
? "rgba(88, 96, 105, 0.2)"
|
||||
: "transparent",
|
||||
padding: "2px 10px",
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
item.type === "addition"
|
||||
? "#28a745"
|
||||
: item.type === "deletion"
|
||||
? "#cb2431"
|
||||
: item.type === "info"
|
||||
? "#586069"
|
||||
: "#abb2bf",
|
||||
}}
|
||||
>
|
||||
{item.line}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
}, [content, style]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
// maxHeight: 500,
|
||||
position: "relative",
|
||||
overflow: "auto",
|
||||
// border: '1px solid #ddd',
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
alias/frontend/src/components/Viewer/HtmlViewer.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useState, useEffect, ReactNode } from "react";
|
||||
import { BaseViewerProps } from "./types";
|
||||
|
||||
export const HtmlViewer: React.FC<BaseViewerProps> = ({ content, style }) => {
|
||||
const [component, setComponent] = useState<ReactNode>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setComponent(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
srcDoc={content}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
border: "none",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
height: 500,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
227
alias/frontend/src/components/Viewer/MarkdownViewer.module.scss
Normal file
@@ -0,0 +1,227 @@
|
||||
.markdown {
|
||||
// Basic text styles
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
// Ensure container does not exceed parent width
|
||||
width: 100%;
|
||||
// Limit maximum width
|
||||
max-width: 100%;
|
||||
// overflow: 'hidden',
|
||||
|
||||
// Heading styles
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0.3em 0 0.1em;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
// Paragraph styles
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
&+p {
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// List styles
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em 0;
|
||||
padding-left: 1.2em;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&+li {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
// Code styles
|
||||
code {
|
||||
padding: 0.1em 0.2em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
margin: 0.2em 0;
|
||||
font-size: 85%;
|
||||
line-height: 1.4;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Quote styles
|
||||
blockquote {
|
||||
margin: 0.2em 0;
|
||||
padding: 0 0.5em;
|
||||
color: #656d76;
|
||||
border-left: 0.2em solid #d0d7de;
|
||||
}
|
||||
|
||||
// Table styles
|
||||
table {
|
||||
margin: 0.2em 0;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d0d7de;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #d0d7de;
|
||||
|
||||
&:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image styles
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
// Divider styles
|
||||
hr {
|
||||
height: 1px;
|
||||
margin: 0.2em 0;
|
||||
background-color: #d0d7de;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Link styles
|
||||
a {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Specifically handle spaces created by line breaks
|
||||
br {
|
||||
// Changed to inline display
|
||||
display: inline;
|
||||
// Preserve line break effect
|
||||
white-space: pre;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 0.3em;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// Handle consecutive line breaks
|
||||
br+br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Handle line breaks in paragraphs
|
||||
p {
|
||||
br {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
height: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle line breaks in list items
|
||||
li {
|
||||
br {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
height: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
// Task list styles
|
||||
input[type="checkbox"] {
|
||||
margin: 0 0.3em 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Task list item styles
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
margin-left: -1.2em;
|
||||
padding-left: 1.2em;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.2em;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove extra spacing from first and last elements
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
18
alias/frontend/src/components/Viewer/MarkdownViewer.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// src/viewers/MarkdownViewer.ts
|
||||
import React from "react";
|
||||
import { BaseViewerProps } from "./types";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { markdownRegex } from "@/utils/constant";
|
||||
|
||||
export const MarkdownViewer: React.FC<BaseViewerProps> = ({
|
||||
content,
|
||||
style,
|
||||
}) => {
|
||||
const processed = content?.match(markdownRegex)?.[1] || content;
|
||||
return (
|
||||
<div style={style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{processed}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
81
alias/frontend/src/components/Viewer/UniversalViewer.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { getFileType, languageMap } from "./utils";
|
||||
import { HtmlViewer } from "./HtmlViewer";
|
||||
import { MarkdownViewer } from "./MarkdownViewer";
|
||||
import { CodeViewer } from "./CodeViewer";
|
||||
import { CSVViewer } from "./CSVViewer";
|
||||
import { ChartViewer } from "./ChartViewer";
|
||||
import { DiffViewer } from "./DiffViewer";
|
||||
import { ViewerStyle } from "./types";
|
||||
|
||||
interface UniversalViewerProps {
|
||||
content: string;
|
||||
fileName?: string;
|
||||
style?: ViewerStyle;
|
||||
}
|
||||
|
||||
export const UniversalViewer: React.FC<UniversalViewerProps> = ({
|
||||
content,
|
||||
fileName = "",
|
||||
style = {},
|
||||
}) => {
|
||||
const getViewer = () => {
|
||||
const defaultStyles = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
...style,
|
||||
};
|
||||
|
||||
const fileType = fileName?.split(".").pop()?.toLowerCase() || "";
|
||||
const type = getFileType(fileType);
|
||||
switch (type) {
|
||||
case "html":
|
||||
return <HtmlViewer content={content} style={defaultStyles} />;
|
||||
|
||||
case "markdown":
|
||||
return <MarkdownViewer content={content} style={defaultStyles} />;
|
||||
|
||||
case "csv":
|
||||
return <CSVViewer content={content} style={defaultStyles} />;
|
||||
|
||||
// case 'pdf':
|
||||
// return <PDFViewer content={content} style={defaultStyles} />;
|
||||
|
||||
// case 'image':
|
||||
// return <ImageViewer content={content} style={defaultStyles} />;
|
||||
|
||||
case "chart":
|
||||
return <ChartViewer content={content} style={defaultStyles} />;
|
||||
|
||||
case "diff":
|
||||
return <DiffViewer content={content} style={defaultStyles} />;
|
||||
|
||||
default:
|
||||
const language = languageMap[fileType] || "text";
|
||||
return (
|
||||
<CodeViewer
|
||||
content={content}
|
||||
language={language}
|
||||
style={defaultStyles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
// maxHeight: '500px',
|
||||
position: "relative",
|
||||
// overflow: 'auto',
|
||||
// border: '1px solid #ddd',
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{getViewer()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
8
alias/frontend/src/components/Viewer/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export { UniversalViewer } from "./UniversalViewer";
|
||||
export { MarkdownViewer } from "./MarkdownViewer";
|
||||
export { CodeViewer } from "./CodeViewer";
|
||||
export { CSVViewer } from "./CSVViewer";
|
||||
export { HtmlViewer } from "./HtmlViewer";
|
||||
export { DiffViewer } from "./DiffViewer";
|
||||
export { ChartViewer } from "./ChartViewer";
|
||||
export type { ViewerStyle, BaseViewerProps } from "./types";
|
||||
256
alias/frontend/src/components/Viewer/styles.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// src/viewers/styles.ts
|
||||
|
||||
export const COMMON_STYLES = `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
padding: 30px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Markdown heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
position: relative;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
cursor: text;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25em;
|
||||
border-bottom: 1px solid #eceff1;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
border-bottom: 1px solid #eceff1;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
/* Code block styles */
|
||||
pre {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Cascadia Code', 'Fira Code', Consolas, 'Courier New', monospace;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5em;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table tr {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #c6cbd1;
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: bold;
|
||||
background-color: #f0f0f0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Blockquote styles */
|
||||
blockquote {
|
||||
border-left: 4px solid #dfe2e5;
|
||||
padding: 0 15px;
|
||||
color: #777;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul, ol {
|
||||
padding-left: 30px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Horizontal line */
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: #e1e4e8;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* Additional styles for inline code and code blocks */
|
||||
.highlight {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.linenos {
|
||||
background-color: #f0f0f0;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Image styles */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* Paragraph styles */
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Emphasis styles */
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Strikethrough styles */
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Superscript and subscript */
|
||||
sup, sub {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Keyboard input styles */
|
||||
kbd {
|
||||
background-color: #fafbfc;
|
||||
border: 1px solid #d1d5da;
|
||||
border-bottom-color: #c6cbd1;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #c6cbd1;
|
||||
color: #444d56;
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
padding: 3px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Text selection styles */
|
||||
::selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Scrollbar styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HIGHLIGHT_STYLES = {
|
||||
tomorrow: {
|
||||
"hljs-comment": {
|
||||
color: "#999999",
|
||||
},
|
||||
"hljs-keyword": {
|
||||
color: "#cc99cd",
|
||||
},
|
||||
"hljs-string": {
|
||||
color: "#7ec699",
|
||||
},
|
||||
},
|
||||
};
|
||||
8
alias/frontend/src/components/Viewer/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type ViewerStyle = {
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
export interface BaseViewerProps {
|
||||
content: string;
|
||||
style?: ViewerStyle;
|
||||
}
|
||||
66
alias/frontend/src/components/Viewer/utils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const getFileType = (extension: string) => {
|
||||
switch (extension.toLowerCase().replace(".", "")) {
|
||||
case "html":
|
||||
case "htm":
|
||||
return "html";
|
||||
case "md":
|
||||
case "markdown":
|
||||
return "markdown";
|
||||
case "txt":
|
||||
return "text";
|
||||
case "json":
|
||||
return "json";
|
||||
case "csv":
|
||||
return "csv";
|
||||
case "xml":
|
||||
return "xml";
|
||||
case "yaml":
|
||||
case "yml":
|
||||
return "yaml";
|
||||
case "log":
|
||||
return "log";
|
||||
case "pdf":
|
||||
return "pdf";
|
||||
case "png":
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "gif":
|
||||
case "webp":
|
||||
return "image";
|
||||
case "diff":
|
||||
case "patch":
|
||||
return "diff";
|
||||
case "chart":
|
||||
return "chart";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
};
|
||||
export const languageMap: { [key: string]: string } = {
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
ts: "typescript",
|
||||
tsx: "typescript",
|
||||
py: "python",
|
||||
python: "python",
|
||||
rb: "ruby",
|
||||
java: "java",
|
||||
cpp: "cpp",
|
||||
c: "c",
|
||||
cs: "csharp",
|
||||
go: "go",
|
||||
rs: "rust",
|
||||
php: "php",
|
||||
html: "html",
|
||||
css: "css",
|
||||
scss: "scss",
|
||||
sql: "sql",
|
||||
yaml: "yaml",
|
||||
yml: "yaml",
|
||||
json: "json",
|
||||
xml: "xml",
|
||||
md: "markdown",
|
||||
sh: "bash",
|
||||
bash: "bash",
|
||||
diff: "diff",
|
||||
};
|
||||
91
alias/frontend/src/components/Workspace/index.module.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
.workspaceHeader {
|
||||
@apply flex items-center justify-between;
|
||||
.titleContainer {
|
||||
@apply flex items-center;
|
||||
|
||||
.title {
|
||||
@apply font-alibaba text-[17px] font-bold leading-[28px] text-text-primary;
|
||||
}
|
||||
}
|
||||
.computerIcon {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.collectButton {
|
||||
@apply flex items-center px-4 py-1 rounded-[12px] border border-black;
|
||||
background-color: var(--message-content);
|
||||
|
||||
img {
|
||||
@apply w-5 h-5 mr-1;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply font-alibaba text-[13px] font-bold leading-[22px] text-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.todoHeader {
|
||||
@apply flex items-center mt-1;
|
||||
|
||||
.stepIcon {
|
||||
@apply w-5 h-5 mr-[6px];
|
||||
}
|
||||
|
||||
.stepCount {
|
||||
@apply px-2 py-[2px] mr-[5px] rounded-[8px];
|
||||
@apply font-alibaba text-[13px] font-bold leading-[22px];
|
||||
background-color: var(--sps-color-mauve-bg);
|
||||
color: var(--sps-color-mauve);
|
||||
}
|
||||
|
||||
.stepTitle {
|
||||
@apply font-alibaba text-[15px] leading-6 tracking-[0.2px] text-black;
|
||||
width: 100%;
|
||||
}
|
||||
.workspaceSelect {
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.todoList {
|
||||
@apply mx-0 my-[14px] mb-[22px];
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0;
|
||||
}
|
||||
:global {
|
||||
pre {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
code {
|
||||
color: var(--sps-color-text-secondary) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
// .collapse {
|
||||
// :global {
|
||||
// .sps-collapse-content-box {
|
||||
// padding: 0 !important;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.workWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 20px;
|
||||
border-top: solid 1px var(--sps-color-border-secondary);
|
||||
.workspaceHeader {
|
||||
height: 52px;
|
||||
}
|
||||
}
|
||||
.renderLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.labelId {
|
||||
color: var(--sps-color-text-tertiary);
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
380
alias/frontend/src/components/Workspace/index.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { useWorkspace } from "@/context/WorkspaceContext.tsx";
|
||||
import { Message, MessageType, ToolCallMessage } from "@/types/message";
|
||||
import { Collapse, CollapseProps, Select } from "@agentscope-ai/design";
|
||||
import { SparkComputerLine } from "@agentscope-ai/icons";
|
||||
import type { SelectProps } from "antd";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark, prism } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { UniversalViewer } from "../Viewer";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const Workspace = () => {
|
||||
const {
|
||||
displayedContent,
|
||||
args,
|
||||
messageList = [],
|
||||
setDisplayedContent,
|
||||
} = useWorkspace();
|
||||
const { theme } = useTheme();
|
||||
const [serialNum, setSerialNum] = useState<number>(0);
|
||||
const [useToolInfo, setUseToolInfo] = useState<{
|
||||
name: string;
|
||||
arguments: string;
|
||||
type: string;
|
||||
}>({ name: "", arguments: "", type: "" });
|
||||
type LabelRender = SelectProps["labelRender"];
|
||||
const updateToolInfo = (toolMsg?: ToolCallMessage) => {
|
||||
if (
|
||||
toolMsg &&
|
||||
toolMsg.arguments &&
|
||||
Object.keys(toolMsg.arguments).length > 0
|
||||
) {
|
||||
setUseToolInfo({
|
||||
name: toolMsg.tool_name || "",
|
||||
arguments: JSON.stringify(toolMsg.arguments, null, 2),
|
||||
type: toolMsg.type,
|
||||
});
|
||||
} else {
|
||||
setUseToolInfo({
|
||||
name: "",
|
||||
arguments: "",
|
||||
type: "",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (displayedContent) {
|
||||
const selectedMessage = messageList.find(
|
||||
(msg: Message) => msg.content === displayedContent,
|
||||
);
|
||||
if (selectedMessage) {
|
||||
findIndex(selectedMessage.id);
|
||||
if ((selectedMessage as ToolCallMessage).arguments) {
|
||||
const toolMsg = selectedMessage as ToolCallMessage;
|
||||
updateToolInfo(toolMsg);
|
||||
} else {
|
||||
updateToolInfo(); // Set to null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateToolInfo(); // Set to null
|
||||
}
|
||||
}, [displayedContent, messageList]);
|
||||
|
||||
const customStyle = {
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
// maxHeight: 500,
|
||||
overflow: "auto",
|
||||
padding: 0,
|
||||
backgroundColor: "var(--sps-color-bg-base)",
|
||||
};
|
||||
const renderMarkdown = (content: string, language: string = "text") => {
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
style={theme === "dark" ? oneDark : prism}
|
||||
customStyle={theme === "dark" ? customStyle : { ...customStyle }}
|
||||
>
|
||||
{content}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
};
|
||||
const findIndex = (v: string) => {
|
||||
const index = messageList.findIndex((item) => item.id === v);
|
||||
setSerialNum(index >= 0 ? index + 1 : 0);
|
||||
};
|
||||
const getBaseLabel = () => {
|
||||
if (!!displayedContent && typeof displayedContent === "string") {
|
||||
const displayedContentObj = JSON.parse(displayedContent);
|
||||
if (
|
||||
Array.isArray(displayedContentObj) &&
|
||||
displayedContentObj.length > 0
|
||||
) {
|
||||
if (displayedContentObj[0].type === MessageType.TOOL_USE)
|
||||
return "Raw data";
|
||||
if (displayedContentObj[0].type === MessageType.TOOL_RESULT)
|
||||
return "Tool result";
|
||||
}
|
||||
if (displayedContentObj.hasOwnProperty("type")) {
|
||||
if (displayedContentObj.type === MessageType.TOOL_USE)
|
||||
return "Raw data";
|
||||
if (displayedContentObj.type === MessageType.TOOL_RESULT)
|
||||
return "Tool result";
|
||||
}
|
||||
}
|
||||
return "Raw result";
|
||||
};
|
||||
// Create an array that refreshes each time displayedContent changes
|
||||
const items: CollapseProps["items"] = useMemo(() => {
|
||||
const base: CollapseProps["items"] = [];
|
||||
if (displayedContent === null) {
|
||||
return [];
|
||||
}
|
||||
const currentMessage = messageList.find(
|
||||
(msg: Message) => msg.content === displayedContent,
|
||||
);
|
||||
if (currentMessage) {
|
||||
findIndex(currentMessage.id);
|
||||
}
|
||||
base.push({
|
||||
key: "1",
|
||||
label: getBaseLabel(),
|
||||
children: (
|
||||
<SyntaxHighlighter
|
||||
language="JSON"
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
style={theme === "dark" ? oneDark : prism}
|
||||
// Background color change
|
||||
customStyle={
|
||||
theme === "dark"
|
||||
? customStyle
|
||||
: { ...customStyle, background: "transparent" }
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(displayedContent), null, 2);
|
||||
} catch (error) {
|
||||
return displayedContent;
|
||||
}
|
||||
})()}
|
||||
</SyntaxHighlighter>
|
||||
),
|
||||
});
|
||||
|
||||
try {
|
||||
const toolResultBlocks = JSON.parse(displayedContent);
|
||||
if (Array.isArray(toolResultBlocks)) {
|
||||
const toolResultBlock = toolResultBlocks[0];
|
||||
if (
|
||||
toolResultBlock.hasOwnProperty("name") &&
|
||||
toolResultBlock.hasOwnProperty("output")
|
||||
) {
|
||||
const { name, output } = toolResultBlock;
|
||||
// show output 0 for temp
|
||||
switch (name) {
|
||||
case "edit_file":
|
||||
base.unshift({
|
||||
key: "output-0",
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: (
|
||||
<UniversalViewer
|
||||
content={output[0].text}
|
||||
fileName="fake.diff"
|
||||
style={customStyle}
|
||||
/>
|
||||
),
|
||||
});
|
||||
return base;
|
||||
case "read_file":
|
||||
base.unshift({
|
||||
key: "output-0",
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: (
|
||||
<UniversalViewer
|
||||
content={output[0].text}
|
||||
fileName={args?.path}
|
||||
style={customStyle}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return base;
|
||||
case "write_file":
|
||||
base.unshift({
|
||||
key: "output-0",
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: (
|
||||
<UniversalViewer
|
||||
content={args?.content || output[0].text}
|
||||
fileName={args?.path}
|
||||
style={customStyle}
|
||||
/>
|
||||
),
|
||||
});
|
||||
return base;
|
||||
case "generate_chart":
|
||||
base.unshift({
|
||||
key: "output-0",
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: (
|
||||
<UniversalViewer
|
||||
content={output[0].text}
|
||||
fileName="fake.chart"
|
||||
style={customStyle}
|
||||
/>
|
||||
),
|
||||
});
|
||||
return base;
|
||||
}
|
||||
|
||||
if (Array.isArray(output)) {
|
||||
output.forEach((item, index) => {
|
||||
if (item.hasOwnProperty("type")) {
|
||||
switch (item.type) {
|
||||
case "text":
|
||||
if (base[0].key === "image") {
|
||||
// Insert at second position
|
||||
base.splice(1, 0, {
|
||||
key: `output-${index}`,
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: renderMarkdown(item.text),
|
||||
});
|
||||
} else {
|
||||
base.unshift({
|
||||
key: `output-${index}`,
|
||||
label: `Output of 🛠️ ${name}`,
|
||||
children: renderMarkdown(item.text),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "image":
|
||||
base.unshift({
|
||||
key: `image-${index}`,
|
||||
label: "Image",
|
||||
children: (
|
||||
<img
|
||||
src={"data:image/jpeg;base64," + item.data}
|
||||
alt="Image"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: 500,
|
||||
overflow: "auto",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
useToolInfo.arguments &&
|
||||
useToolInfo.name &&
|
||||
useToolInfo.type === MessageType.TOOL_USE
|
||||
) {
|
||||
base.unshift({
|
||||
key: "input",
|
||||
label: `Arguments of 🛠️ ${useToolInfo.name}`,
|
||||
children: renderMarkdown(useToolInfo.arguments),
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return base;
|
||||
}, [displayedContent, args, theme, useToolInfo]);
|
||||
|
||||
const selectOnchange = (v: string) => {
|
||||
// v is now id, need to find corresponding message based on id
|
||||
const selectedMessage = messageList.find((msg: Message) => msg.id === v);
|
||||
if (selectedMessage && selectedMessage.content !== displayedContent) {
|
||||
findIndex(v);
|
||||
setDisplayedContent(selectedMessage.content);
|
||||
// Find corresponding message and set arguments
|
||||
if ((selectedMessage as ToolCallMessage).arguments) {
|
||||
const toolMsg = selectedMessage as ToolCallMessage;
|
||||
updateToolInfo(toolMsg);
|
||||
} else {
|
||||
updateToolInfo(); // Set to null
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(
|
||||
`message-toolcall-${selectedMessage.id}`,
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
const renderLabel = (d: ToolCallMessage | Message) => {
|
||||
let prefixName = `Output of `;
|
||||
if (d?.type === MessageType.TOOL_USE) prefixName = `Using tool input of `;
|
||||
if (d?.type === MessageType.TOOL_RESULT)
|
||||
prefixName = `Tool result output of `;
|
||||
return (
|
||||
<div className={styles.renderLabel}>
|
||||
<span> {`${prefixName}🛠️ ${d?.tool_name ?? "Unknown Tool"}`}</span>
|
||||
<span className={styles.labelId}>{d.id}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props;
|
||||
// Find corresponding message based on value (id)
|
||||
const message = messageList.find((msg: Message) => msg.id === value);
|
||||
if (!message) {
|
||||
return <span>No option match</span>;
|
||||
}
|
||||
const d = message as ToolCallMessage;
|
||||
let prefixName = `Output of `;
|
||||
if (d?.type === MessageType.TOOL_USE) prefixName = `Using tool input of `;
|
||||
if (d?.type === MessageType.TOOL_RESULT)
|
||||
prefixName = `Tool result output of `;
|
||||
return <span>{`${prefixName}🛠️ ${d?.tool_name ?? "Unknown Tool"}`}</span>;
|
||||
};
|
||||
return (
|
||||
<div className={styles.workWrap}>
|
||||
<div className={styles.workspaceHeader}>
|
||||
<div className={styles.titleContainer}>
|
||||
<SparkComputerLine className={styles.computerIcon} />
|
||||
<h2 className={styles.title}>Agent Workspace</h2>
|
||||
</div>
|
||||
</div>
|
||||
{displayedContent && (
|
||||
<div className={styles.todoHeader}>
|
||||
<span className={styles.stepTitle}>
|
||||
<Select
|
||||
className={styles.workspaceSelect}
|
||||
prefix={
|
||||
<span className={styles.stepCount}>
|
||||
{serialNum}/{messageList?.length}
|
||||
</span>
|
||||
}
|
||||
value={
|
||||
messageList.find(
|
||||
(msg: Message) => msg.content === displayedContent,
|
||||
)?.id
|
||||
}
|
||||
onChange={(v) => {
|
||||
selectOnchange(v);
|
||||
}}
|
||||
notFoundContent={null}
|
||||
options={(messageList || []).map((d: Message) => ({
|
||||
value: d.id,
|
||||
label: renderLabel(d),
|
||||
// disabled: displayedContent === null
|
||||
}))}
|
||||
labelRender={labelRender}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.todoList}>
|
||||
{displayedContent ? (
|
||||
<Collapse
|
||||
items={items}
|
||||
className={styles.collapse}
|
||||
defaultActiveKey={
|
||||
items.length === 1 ? ["1"] : ["file", "output", "image"]
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Workspace);
|
||||
34
alias/frontend/src/context/MessageContext.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Conversation } from "@/types/api";
|
||||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
interface MessageContextType {
|
||||
currentConversation: Conversation | null; // Modified to allow null type
|
||||
setCurrentConversation: (conversation: Conversation | null) => void;
|
||||
}
|
||||
|
||||
const MessageContext = createContext<MessageContextType | undefined>(undefined);
|
||||
|
||||
export const MessageProvider: React.FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [currentConversation, setCurrentConversation] =
|
||||
useState<Conversation | null>(null);
|
||||
return (
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
currentConversation,
|
||||
setCurrentConversation,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMessage = () => {
|
||||
const context = useContext(MessageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useMessage must be used within a MessageProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
59
alias/frontend/src/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const savedTheme = localStorage.getItem("theme") as Theme;
|
||||
return savedTheme || "system";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
const applyTheme = () => {
|
||||
let effectiveTheme = theme;
|
||||
if (theme === "system") {
|
||||
effectiveTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(effectiveTheme);
|
||||
localStorage.setItem("theme", effectiveTheme);
|
||||
};
|
||||
|
||||
applyTheme();
|
||||
if (theme === "system") {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = () => applyTheme();
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
53
alias/frontend/src/context/WorkspaceContext.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Message } from "@/types/message";
|
||||
interface WorkspaceContextType {
|
||||
displayedContent: string | null;
|
||||
args: Record<string, any>;
|
||||
setDisplayedContent: Dispatch<SetStateAction<string | null>>;
|
||||
setArgs: Dispatch<SetStateAction<Record<string, any>>>;
|
||||
activeKey: string;
|
||||
setActiveKey: Dispatch<SetStateAction<string>>;
|
||||
messageList: Message[];
|
||||
setMessageList: Dispatch<SetStateAction<Message[]>>;
|
||||
}
|
||||
|
||||
const WorkspaceContext = createContext<WorkspaceContextType | null>(null);
|
||||
|
||||
export function WorkspaceProvider({ children }: { children: ReactNode }) {
|
||||
const [displayedContent, setDisplayedContent] = useState<string | null>(null);
|
||||
const [args, setArgs] = useState<Record<string, any>>({});
|
||||
|
||||
const [activeKey, setActiveKey] = useState("1");
|
||||
const [messageList, setMessageList] = useState<Message[]>([]);
|
||||
return (
|
||||
<WorkspaceContext.Provider
|
||||
value={{
|
||||
displayedContent,
|
||||
setDisplayedContent,
|
||||
activeKey,
|
||||
setActiveKey,
|
||||
args,
|
||||
setArgs,
|
||||
messageList,
|
||||
setMessageList,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkspaceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWorkspace() {
|
||||
const context = useContext(WorkspaceContext);
|
||||
if (!context) {
|
||||
throw new Error("useWorkspace must be used within a WorkspaceProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
35
alias/frontend/src/interfaces/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Common response interface
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// AI conversation related interface
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
type: "text" | "code" | "image" | "mindmap";
|
||||
}
|
||||
|
||||
// AI solution related interface
|
||||
export interface Solution {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
steps: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
status: "pending" | "in-process" | "completed";
|
||||
}>;
|
||||
}
|
||||
|
||||
// User settings interface
|
||||
export interface UserSettings {
|
||||
theme: "light" | "dark" | "system";
|
||||
language: "zh" | "en";
|
||||
fontSize: number;
|
||||
}
|
||||
15
alias/frontend/src/main.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles/global.scss";
|
||||
|
||||
// Enable hot reload
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept();
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
60
alias/frontend/src/pages/Chat/Avatar/index.module.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
.headerButton {
|
||||
@apply w-10 h-10 flex items-center justify-center rounded-lg transition-all duration-200 ease-in-out;
|
||||
|
||||
img {
|
||||
@apply opacity-60 transition-all duration-200;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
@apply opacity-100 scale-105;
|
||||
}
|
||||
}
|
||||
|
||||
.userPopover {
|
||||
:global {
|
||||
.sps-popover-content {
|
||||
.sps-popover-inner {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popoverAvatarContent {
|
||||
min-width: 160px;
|
||||
min-height: 106px;
|
||||
padding: 5px;
|
||||
|
||||
.userInfo {
|
||||
padding: 8px
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply w-8 h-8 rounded-full bg-gray-200 overflow-hidden mr-4;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--sps-color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email {
|
||||
color: var(--sps-color-text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.userButton {
|
||||
border-top: solid 1px var(--sps-color-border-secondary);
|
||||
padding: 12px 0;
|
||||
|
||||
.item {
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sps-color-fill-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
alias/frontend/src/pages/Chat/Avatar/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import UserHeader from "@/assets/icons/avatar/avatar.svg";
|
||||
import { Popover } from "@agentscope-ai/design";
|
||||
import { SparkEscapeLine, SparkSwitchLine } from "@agentscope-ai/icons";
|
||||
import { Flex } from "antd";
|
||||
import React, { memo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface UserInfoProps {
|
||||
uid: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
}
|
||||
interface AvatarProps {
|
||||
userInfo: UserInfoProps;
|
||||
}
|
||||
const Avatar: React.FC<AvatarProps> = ({ userInfo }) => {
|
||||
const navigate = useNavigate();
|
||||
const logOutHandle = () => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
navigate("/login?mode=login");
|
||||
};
|
||||
return (
|
||||
<Popover
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
overlayClassName={styles.userPopover}
|
||||
content={
|
||||
<div className={styles.popoverAvatarContent}>
|
||||
<Flex className={styles.userInfo}>
|
||||
<div className={styles.header}>
|
||||
<img
|
||||
src={UserHeader}
|
||||
alt="User"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.name}>{userInfo?.userName || ""}</div>
|
||||
<div className={styles.email}>{userInfo?.email || ""}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex vertical className={styles.userButton}>
|
||||
<Flex
|
||||
gap="small"
|
||||
align="center"
|
||||
className={styles.item}
|
||||
onClick={logOutHandle}
|
||||
>
|
||||
<SparkSwitchLine style={{ fontSize: 18 }} />
|
||||
Switch account
|
||||
</Flex>
|
||||
<Flex
|
||||
gap="small"
|
||||
align="center"
|
||||
className={styles.item}
|
||||
onClick={logOutHandle}
|
||||
>
|
||||
<SparkEscapeLine style={{ fontSize: 18 }} /> Log out
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button className={styles.headerButton}>
|
||||
<div className="w-8 h-8 rounded-full bg-gray-200 overflow-hidden">
|
||||
<img
|
||||
src={UserHeader}
|
||||
alt="User"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Avatar);
|
||||
85
alias/frontend/src/pages/Chat/ChatHeader/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { memo, useEffect, useState } from "react";
|
||||
import { Conversation } from "@/types/api";
|
||||
import { Flex } from "antd";
|
||||
import {
|
||||
SparkStarLine,
|
||||
SparkStarFill,
|
||||
SparkSettingLine,
|
||||
SparkShareLine,
|
||||
SparkMessageLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import { ShareModal } from "@/components/ShareModal";
|
||||
|
||||
interface ChatHeaderProps {
|
||||
currentConversation: Conversation | null;
|
||||
languageType: string;
|
||||
setLanguageType: (type: string) => void;
|
||||
setCurrentConversation: (con: Conversation) => void;
|
||||
}
|
||||
const iconStyle = {
|
||||
fontSize: "20px",
|
||||
cursor: "pointer",
|
||||
marginRight: "12px",
|
||||
};
|
||||
const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
currentConversation,
|
||||
languageType,
|
||||
setCurrentConversation,
|
||||
}) => {
|
||||
const [nowConversation, setNowConversation] = useState<Conversation | null>(
|
||||
null,
|
||||
);
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentConversation) setNowConversation(currentConversation);
|
||||
}, [currentConversation]);
|
||||
|
||||
const collectedHandle = async () => {
|
||||
if (!nowConversation) return;
|
||||
const response = await conversationApi.collect(
|
||||
nowConversation.id,
|
||||
!nowConversation.collected,
|
||||
);
|
||||
if (response?.payload) {
|
||||
setCurrentConversation(response?.payload as Conversation);
|
||||
}
|
||||
};
|
||||
const shareHandle = () => {
|
||||
setIsShareModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex className="h-[56px] pt-4 ml-4 mr-6" justify="space-between">
|
||||
<Flex className="p-2 w-4/5">
|
||||
<SparkMessageLine style={{ fontSize: 20 }} />
|
||||
<div className="ml-2 mr-2">{nowConversation?.name}</div>
|
||||
<div onClick={collectedHandle}>
|
||||
{!nowConversation?.collected && <SparkStarLine style={iconStyle} />}
|
||||
{nowConversation?.collected && (
|
||||
<SparkStarFill style={{ ...iconStyle, color: "#ED7E2F" }} />
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
{/* <Flex className="w-1/5 " justify="flex-end">
|
||||
<SparkShareLine style={iconStyle} onClick={shareHandle} />
|
||||
<SparkSettingLine style={iconStyle} />
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
{isShareModalOpen && (
|
||||
<ShareModal
|
||||
isOpen={isShareModalOpen}
|
||||
onClose={() => setIsShareModalOpen(false)}
|
||||
shared={nowConversation?.shared || false}
|
||||
conversationId={nowConversation?.id || ""}
|
||||
setCurrentConversation={setCurrentConversation}
|
||||
shareUrl={`${window.location.origin}/share/${nowConversation?.user_id}/${nowConversation?.id}?replay=1`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatHeader);
|
||||
16
alias/frontend/src/pages/Chat/ChatInput/index.module.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.chatInput {
|
||||
:global {
|
||||
.sps-sender-mode-select-display-flex {
|
||||
.spark-icon {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
background-color: #0b83f1;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
295
alias/frontend/src/pages/Chat/ChatInput/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import { fileApi } from "@/services/api/file";
|
||||
import { FilePreview } from "@/types/api";
|
||||
import { ChatModeList, MAX_FILE_SIZE } from "@/utils/constant";
|
||||
import { formatFileSize, getUniqueFileName } from "@/utils/fileNameUtils";
|
||||
import {
|
||||
Attachments,
|
||||
Disclaimer,
|
||||
ChatInput as Input,
|
||||
} from "@agentscope-ai/chat";
|
||||
import { IconButton, message } from "@agentscope-ai/design";
|
||||
import { SparkAttachmentLine } from "@agentscope-ai/icons";
|
||||
import { GetProp, Upload } from "antd";
|
||||
import React, { memo, useMemo } from "react";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
// Define file item type
|
||||
type AttachmentItem = GetProp<typeof Attachments, "items">[number];
|
||||
|
||||
interface ChatInputProps {
|
||||
handleSendMessage: (value: string) => void;
|
||||
setFilePreview: React.Dispatch<React.SetStateAction<FilePreview[]>>;
|
||||
setValue?: (value: string) => void;
|
||||
value?: string;
|
||||
conversationId?: string;
|
||||
isGenerating?: boolean;
|
||||
stopGenerating?: () => void;
|
||||
taskId?: string;
|
||||
chatMode: string;
|
||||
filePreview: FilePreview[];
|
||||
}
|
||||
const createModeLabel = (text: string) => (
|
||||
<div className="flex items-center">
|
||||
<div className={styles.badge}></div>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
conversationId,
|
||||
isGenerating,
|
||||
value,
|
||||
taskId,
|
||||
handleSendMessage,
|
||||
setFilePreview,
|
||||
setValue,
|
||||
stopGenerating,
|
||||
chatMode,
|
||||
filePreview,
|
||||
}) => {
|
||||
const [attachedFiles, setAttachedFiles] = React.useState<AttachmentItem[]>(
|
||||
[],
|
||||
);
|
||||
const options = useMemo(() => {
|
||||
const getChatModeLabel = () => {
|
||||
const mode = ChatModeList.find((mode) => mode.value === chatMode);
|
||||
return mode ? `${mode.label} · Ready` : "General Mode · Ready";
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
icon: "",
|
||||
label: createModeLabel(getChatModeLabel()),
|
||||
value: chatMode,
|
||||
tooltip: "",
|
||||
},
|
||||
{
|
||||
icon: "",
|
||||
label: createModeLabel("Alias is handling the task..."),
|
||||
value: "isGenerating",
|
||||
tooltip: "",
|
||||
},
|
||||
];
|
||||
}, [chatMode]);
|
||||
const handleCustomRequest = async (options: {
|
||||
file: File;
|
||||
onSuccess: Function;
|
||||
onError: Function;
|
||||
onProgress: Function;
|
||||
}) => {
|
||||
const { file, onSuccess, onError, onProgress } = options;
|
||||
try {
|
||||
// Merge file names from server file list and local preview list
|
||||
let existingFiles: string[] = [];
|
||||
if (conversationId) {
|
||||
// Get file list for current conversation
|
||||
const response: any = await conversationApi.getFiles(conversationId);
|
||||
if (!response.status || !response.payload) {
|
||||
message.error("Failed to get file list");
|
||||
onError(new Error("Failed to get file list"));
|
||||
return;
|
||||
}
|
||||
existingFiles = [
|
||||
...(response.payload.items || []),
|
||||
...attachedFiles.map(
|
||||
(fp) => `conversations/${conversationId}/${fp.name}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
// Check file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
message.error(`File ${file.name} exceeds 10MB limit`);
|
||||
onError(new Error(`File ${file.name} exceeds 10MB limit`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this file already exists locally (including files being uploaded)
|
||||
const isUploading = attachedFiles.some((fp) => fp.name === file.name);
|
||||
if (isUploading) {
|
||||
message.error(
|
||||
`File ${file.name} is being uploaded, please do not upload again.`,
|
||||
);
|
||||
onError(
|
||||
new Error(
|
||||
`File ${file.name} is being uploaded, please do not upload again`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get unique file name
|
||||
const uniqueFileName = getUniqueFileName(existingFiles, file.name);
|
||||
// Create new File object if file name has changed
|
||||
const renamedFile =
|
||||
uniqueFileName !== file.name
|
||||
? new File([file], uniqueFileName, { type: file.type })
|
||||
: file;
|
||||
|
||||
// Add new file preview
|
||||
const newAttachedFiles: AttachmentItem = {
|
||||
uid: `temp-${Date.now()}-${file.name}`,
|
||||
name: uniqueFileName,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
status: "uploading",
|
||||
};
|
||||
|
||||
const newFilePreview: FilePreview = {
|
||||
name: uniqueFileName,
|
||||
type: file.type,
|
||||
size: formatFileSize(file.size || 0),
|
||||
id: "",
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
setAttachedFiles((prev) => [...prev, newAttachedFiles]);
|
||||
setFilePreview((prev) => [...prev, newFilePreview]);
|
||||
|
||||
// Create upload progress callback
|
||||
const progressCallback = (progressEvent: ProgressEvent) => {
|
||||
const progress = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total,
|
||||
);
|
||||
|
||||
// Update progress
|
||||
setAttachedFiles((prev) =>
|
||||
prev.map((fp) =>
|
||||
fp.name === uniqueFileName ? { ...fp, progress } : fp,
|
||||
),
|
||||
);
|
||||
|
||||
onProgress({ percent: progress });
|
||||
};
|
||||
|
||||
// Execute upload
|
||||
const uploadResponse: any = await fileApi.uploadFile(
|
||||
renamedFile,
|
||||
progressCallback,
|
||||
);
|
||||
|
||||
if (uploadResponse.status && uploadResponse.payload) {
|
||||
const fileId = uploadResponse.payload.id;
|
||||
// Update status to success
|
||||
setAttachedFiles((prev) =>
|
||||
prev.map((fp) =>
|
||||
fp.name === uniqueFileName
|
||||
? {
|
||||
...fp,
|
||||
uid: fileId,
|
||||
status: "done",
|
||||
progress: 100,
|
||||
}
|
||||
: fp,
|
||||
),
|
||||
);
|
||||
setFilePreview((prev) =>
|
||||
prev.map((fp) =>
|
||||
fp.name === uniqueFileName
|
||||
? {
|
||||
...fp,
|
||||
id: fileId,
|
||||
status: "success",
|
||||
progress: 100,
|
||||
}
|
||||
: fp,
|
||||
),
|
||||
);
|
||||
message.success(`File ${uniqueFileName} uploaded successfully`);
|
||||
onSuccess(uploadResponse.payload, file);
|
||||
} else {
|
||||
// Update status to error
|
||||
setAttachedFiles((prev) =>
|
||||
prev.map((fp) =>
|
||||
fp.name === uniqueFileName
|
||||
? {
|
||||
...fp,
|
||||
status: "error",
|
||||
progress: 0,
|
||||
}
|
||||
: fp,
|
||||
),
|
||||
);
|
||||
setFilePreview((prev) =>
|
||||
prev.map((fp) =>
|
||||
fp.name === uniqueFileName
|
||||
? {
|
||||
...fp,
|
||||
status: "error",
|
||||
progress: 0,
|
||||
}
|
||||
: fp,
|
||||
),
|
||||
);
|
||||
message.error(`File ${uniqueFileName} upload failed`);
|
||||
onError(new Error("Upload failed"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Get file name (may have been renamed)
|
||||
const fileName = file.name;
|
||||
message.error(`File ${fileName} upload failed, please try again`);
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
const handleDeleteFileChange: GetProp<
|
||||
typeof Attachments,
|
||||
"onChange"
|
||||
> = async ({ file, fileList }) => {
|
||||
const result = await fileApi.deleteFile(file.uid);
|
||||
if (result.status) {
|
||||
setAttachedFiles(fileList);
|
||||
setFilePreview(filePreview.filter((item) => item.id !== file.uid));
|
||||
message.success(`File ${file.name} deleted`);
|
||||
}
|
||||
};
|
||||
const onSubmit = (value: string) => {
|
||||
if (value) {
|
||||
setAttachedFiles([]);
|
||||
setFilePreview([]);
|
||||
handleSendMessage(value);
|
||||
}
|
||||
};
|
||||
const senderHeader = (
|
||||
<Input.Header closable={false} open={attachedFiles?.length > 0}>
|
||||
<Attachments items={attachedFiles} onChange={handleDeleteFileChange} />
|
||||
</Input.Header>
|
||||
);
|
||||
|
||||
const attachmentsNode = (
|
||||
<Upload
|
||||
fileList={attachedFiles}
|
||||
showUploadList={false}
|
||||
key="upload"
|
||||
multiple
|
||||
maxCount={5}
|
||||
customRequest={handleCustomRequest}
|
||||
>
|
||||
<IconButton icon={<SparkAttachmentLine />} bordered={false} />
|
||||
</Upload>
|
||||
);
|
||||
const onCancel = () => {
|
||||
if (stopGenerating) stopGenerating();
|
||||
};
|
||||
return (
|
||||
<div className={styles.chatInput}>
|
||||
<Input.ModeSelect
|
||||
options={options}
|
||||
value={isGenerating ? "isGenerating" : chatMode}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Please type here..."
|
||||
header={senderHeader}
|
||||
prefix={[attachmentsNode]}
|
||||
loading={isGenerating && taskId ? true : false}
|
||||
onSubmit={onSubmit}
|
||||
onChange={setValue}
|
||||
value={value}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
<Disclaimer desc="AI can also make mistakes. Please use with caution." />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatInput);
|
||||
6
alias/frontend/src/pages/Chat/ChatMode/index.module.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.chatMode {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.chatTag {
|
||||
background-color: var(--sps-color-fill-secondary);;
|
||||
}
|
||||
67
alias/frontend/src/pages/Chat/ChatMode/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { memo } from "react";
|
||||
import { Button } from "@agentscope-ai/design";
|
||||
import { Flex } from "antd";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
SparkBrowseLine,
|
||||
SparkPaperLine,
|
||||
SparkUsdLine,
|
||||
SparkSingleStarLine,
|
||||
SparkDataLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import { ChatModeList, ChatModeType } from "@/utils/constant";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface ChatModeProps {
|
||||
chatModeValue: ChatModeType;
|
||||
setChatModeValue: (type: ChatModeType) => void;
|
||||
}
|
||||
|
||||
const ChatMode: React.FC<ChatModeProps> = ({
|
||||
chatModeValue,
|
||||
setChatModeValue,
|
||||
}) => {
|
||||
const getIcon = (value: string) => {
|
||||
switch (value) {
|
||||
case ChatModeType.GENERAL:
|
||||
return <SparkSingleStarLine />;
|
||||
case ChatModeType.BROWSER:
|
||||
return <SparkBrowseLine />;
|
||||
case ChatModeType.DEEPREASONING:
|
||||
return <SparkPaperLine />;
|
||||
case ChatModeType.FINANCE:
|
||||
return <SparkUsdLine />;
|
||||
case ChatModeType.DATASCIENCE:
|
||||
return <SparkDataLine />;
|
||||
default:
|
||||
return null; // or returning a default icon.
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
gap="small"
|
||||
align="center"
|
||||
justify="center"
|
||||
className={styles.chatMode}
|
||||
>
|
||||
{ChatModeList.map((item) => {
|
||||
return (
|
||||
<Button
|
||||
className={classNames({
|
||||
[styles.chatTag]: item.value === chatModeValue,
|
||||
})}
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
setChatModeValue(item.value);
|
||||
}}
|
||||
>
|
||||
{getIcon(item.value)}
|
||||
{item.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatMode);
|
||||
102
alias/frontend/src/pages/Chat/FilePreview/index.module.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
.filePreviewContainer {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filePreviewItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
padding: 12px;
|
||||
width: fit-content; // Let width adapt to content
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:global(.fileItem) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:global(.fileIcon) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.fileInfoBlock) {
|
||||
margin-left: 8px;
|
||||
flex: 1;
|
||||
min-width: 0; // Allow automatic handling when text overflows
|
||||
}
|
||||
}
|
||||
|
||||
.fileBottom {
|
||||
margin-top: 12px;
|
||||
|
||||
.progressBar {
|
||||
height: 3px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 1.5px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.progressFill {
|
||||
height: 100%;
|
||||
background: #1890ff;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.fileStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; // Changed to justify space between
|
||||
font-size: 11px;
|
||||
|
||||
.uploading {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
alias/frontend/src/pages/Chat/FilePreview/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { FileItems } from "@/components/Chat/FileItems";
|
||||
import { fileApi } from "@/services/api/file";
|
||||
import { FilePreview as filePreviewItem } from "@/types/api";
|
||||
import { FILE_IDS_STORAGE_KEY } from "@/utils/constant";
|
||||
import {
|
||||
getStoredFileIds,
|
||||
removeFileIds,
|
||||
saveFileIds,
|
||||
} from "@/utils/fileNameUtils";
|
||||
import { message } from "@agentscope-ai/design";
|
||||
import { SparkFalseLine } from "@agentscope-ai/icons";
|
||||
import React, { memo, useState } from "react";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface FilePreviewProps {
|
||||
filePreview: filePreviewItem[];
|
||||
setFilePreview: React.Dispatch<React.SetStateAction<filePreviewItem[]>>;
|
||||
conversationId: string;
|
||||
}
|
||||
const FilePreview: React.FC<FilePreviewProps> = ({
|
||||
filePreview,
|
||||
setFilePreview,
|
||||
conversationId,
|
||||
}) => {
|
||||
const [localFileList, setLocalFileList] = useState<Set<string>>(new Set());
|
||||
const handleRemoveFile = async (fileName: string) => {
|
||||
const fileToRemove = filePreview.find((fp) => fp.name === fileName);
|
||||
if (!fileToRemove) return;
|
||||
|
||||
// Cancel upload if file is being uploaded
|
||||
if (fileToRemove.status === "uploading" && fileToRemove.abortController) {
|
||||
fileToRemove.abortController.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete file if it has been uploaded successfully
|
||||
if (fileToRemove.id) {
|
||||
try {
|
||||
await fileApi.deleteFile(fileToRemove.id);
|
||||
setFilePreview((prev) => prev.filter((fp) => fp.name !== fileName));
|
||||
|
||||
// Remove from local file list
|
||||
setLocalFileList((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(fileName);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Remove file ID from local storage
|
||||
if (conversationId) {
|
||||
const currentIds = getStoredFileIds(
|
||||
FILE_IDS_STORAGE_KEY,
|
||||
conversationId,
|
||||
);
|
||||
const newIds =
|
||||
currentIds.filter((id) => id !== fileToRemove.id) || [];
|
||||
if (newIds && newIds?.length > 0)
|
||||
saveFileIds(FILE_IDS_STORAGE_KEY, conversationId, newIds);
|
||||
if (newIds?.length === 0)
|
||||
removeFileIds(FILE_IDS_STORAGE_KEY, conversationId);
|
||||
}
|
||||
|
||||
message.success(`File ${fileName} deleted`);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete file:", error);
|
||||
message.error(`Failed to delete file ${fileName}, please try again`);
|
||||
}
|
||||
} else {
|
||||
// Remove directly from preview list if file upload failed or was not successful
|
||||
console.log("File has no ID, removing directly from list");
|
||||
setFilePreview((prev) => prev.filter((fp) => fp.name !== fileName));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{filePreview.length > 0 && (
|
||||
<div className={styles.filePreviewContainer}>
|
||||
{filePreview.map((file, index) => (
|
||||
<div key={index} className={styles.filePreviewItem}>
|
||||
<FileItems
|
||||
files={[
|
||||
{
|
||||
filename: file.name,
|
||||
size: parseInt(file.size),
|
||||
url: file.id
|
||||
? `/api/v1/files/${file.id}/preview`
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className={styles.fileBottom}>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${file.progress || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.fileStatus}>
|
||||
{file.status === "uploading" && (
|
||||
<span className={styles.uploading}>Uploading...</span>
|
||||
)}
|
||||
{file.status === "success" && (
|
||||
<span className={styles.success}>Upload successful</span>
|
||||
)}
|
||||
{file.status === "error" && (
|
||||
<span className={styles.error}>Upload failed</span>
|
||||
)}
|
||||
<button
|
||||
className={styles.removeButton}
|
||||
onClick={() => handleRemoveFile(file.name)}
|
||||
title="Remove file"
|
||||
>
|
||||
<SparkFalseLine />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FilePreview);
|
||||
27
alias/frontend/src/pages/Chat/Habbit/index.module.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.listItem {
|
||||
position: relative;
|
||||
.textEllipsis {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.actionButtons {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sps-color-fill-secondary);
|
||||
.actionButtons {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
382
alias/frontend/src/pages/Chat/Habbit/index.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import { HabbitApi } from "@/services/api/habbit";
|
||||
import {
|
||||
AlertDialog,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconButton,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Tooltip,
|
||||
} from "@agentscope-ai/design";
|
||||
import {
|
||||
SparkCopyLine,
|
||||
SparkDeleteLine,
|
||||
SparkDownArrowLine,
|
||||
SparkEditLine,
|
||||
SparkMoreLine,
|
||||
SparkPlusLine,
|
||||
SparkSearchLine,
|
||||
SparkUserCheckedLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import { Flex, List } from "antd";
|
||||
import copy from "copy-to-clipboard";
|
||||
import React, { memo, useEffect, useMemo, useState } from "react";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface MetadataProps {
|
||||
session_id: string;
|
||||
is_confirmed: number;
|
||||
}
|
||||
interface HabbitDataProps {
|
||||
content: string;
|
||||
pid: string;
|
||||
uid: string;
|
||||
metadata: MetadataProps;
|
||||
}
|
||||
|
||||
interface HabbitModalProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
uid: string;
|
||||
}
|
||||
const { TextArea } = Input;
|
||||
|
||||
const Header: React.FC<{
|
||||
searchKeyword: string;
|
||||
setSearchKeyword: (value: string) => void;
|
||||
copyHandle: () => void;
|
||||
downHandle: () => void;
|
||||
addHabbit: () => void;
|
||||
}> = ({
|
||||
searchKeyword,
|
||||
setSearchKeyword,
|
||||
copyHandle,
|
||||
downHandle,
|
||||
addHabbit,
|
||||
}) => {
|
||||
const fontSize = { fontSize: "20px" };
|
||||
|
||||
return (
|
||||
<Flex gap="large" justify="space-between" style={{ padding: "20px 0" }}>
|
||||
<Flex style={{ width: "200px" }}>
|
||||
<Input
|
||||
placeholder="Search Knowledge..."
|
||||
prefix={<SparkSearchLine style={fontSize} />}
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap="small">
|
||||
<Button type="text" onClick={copyHandle}>
|
||||
<SparkCopyLine style={fontSize} />
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "1",
|
||||
label: "Download",
|
||||
icon: <SparkDownArrowLine style={{ fontSize: 20 }} />,
|
||||
onClick: downHandle,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Delete",
|
||||
danger: true,
|
||||
icon: <SparkDeleteLine style={{ fontSize: 20 }} />,
|
||||
onClick: () => {
|
||||
AlertDialog.warning({
|
||||
title: "Confirm deletion of all habbits?",
|
||||
children:
|
||||
"Once deleted, it cannot be recovered. Please proceed with caution.",
|
||||
centered: true,
|
||||
okText: "Confirm deletion",
|
||||
onOk: () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button type="text">
|
||||
<SparkMoreLine style={fontSize} />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button type="primary" onClick={addHabbit}>
|
||||
<SparkPlusLine style={fontSize} /> Add Habbit
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const HabbitModal: React.FC<HabbitModalProps> = (props) => {
|
||||
const { open, setOpen, uid } = props;
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [habbitContent, setHabbitContent] = useState("");
|
||||
const [contentBefore, setContentBefore] = useState("");
|
||||
const [pid, setPid] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataList, setDataList] = useState<HabbitDataProps[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
|
||||
const getProfilingData = () => {
|
||||
HabbitApi.getUserProfiling(uid)
|
||||
.then((res: { data: HabbitDataProps[] }) => {
|
||||
const data = res?.data;
|
||||
if (data && Array.isArray(data)) setDataList(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error("Network error");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) getProfilingData();
|
||||
}, [open]);
|
||||
|
||||
// Use useMemo to implement filtering logic
|
||||
const filteredDataList = useMemo(() => {
|
||||
if (!searchKeyword) return dataList;
|
||||
|
||||
return dataList.filter((item) =>
|
||||
item.content.toLowerCase().includes(searchKeyword.toLowerCase()),
|
||||
);
|
||||
}, [dataList, searchKeyword]);
|
||||
|
||||
const addHabbit = () => {
|
||||
setEditOpen(true);
|
||||
setHabbitContent("");
|
||||
setPid("");
|
||||
};
|
||||
|
||||
const editHabbit = (contents: string) => {
|
||||
setEditOpen(true);
|
||||
setHabbitContent(contents);
|
||||
setContentBefore(contents);
|
||||
};
|
||||
|
||||
const copyHandle = () => {
|
||||
if (dataList.length > 0) {
|
||||
const contents = JSON.stringify(dataList, null, 2);
|
||||
if (contents) {
|
||||
copy(contents);
|
||||
message.success("Copied successfully");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const downHandle = () => {
|
||||
if (dataList.length > 0) {
|
||||
try {
|
||||
const contents = JSON.stringify(dataList, null, 2);
|
||||
const blob = new Blob([contents], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "habbits.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
message.error("Download failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false);
|
||||
setSearchKeyword("");
|
||||
};
|
||||
const getTitle = () => {
|
||||
if (pid) return "Edit habbit";
|
||||
return "Add habbit";
|
||||
};
|
||||
|
||||
const onCloseEdit = () => {
|
||||
setEditOpen(false);
|
||||
};
|
||||
const deleteProfiling = async (uid: string, pid: string) => {
|
||||
try {
|
||||
const result = await HabbitApi.deleteProfiling(uid, pid);
|
||||
if (result) {
|
||||
getProfilingData();
|
||||
message.success("successfully deleted habbit!");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Failed to delete habbit");
|
||||
}
|
||||
};
|
||||
const onSure = async () => {
|
||||
if (!habbitContent) {
|
||||
message.info("Please enter habbit");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
if (pid) {
|
||||
// edit habbit
|
||||
if (contentBefore.trim() === habbitContent.trim()) {
|
||||
onCloseEdit();
|
||||
confirmProfiling(uid, pid);
|
||||
return;
|
||||
}
|
||||
const result = await HabbitApi.updateProfiling(
|
||||
uid,
|
||||
pid,
|
||||
contentBefore,
|
||||
habbitContent,
|
||||
);
|
||||
if (result) {
|
||||
getProfilingData();
|
||||
message.success("Habbit edited successfully!");
|
||||
}
|
||||
} else {
|
||||
// add habbit
|
||||
const result = await HabbitApi.addProfiling(uid, habbitContent);
|
||||
if (result) {
|
||||
getProfilingData();
|
||||
message.success("Habbit added successfully!");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Network error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
onCloseEdit();
|
||||
}
|
||||
};
|
||||
const confirmProfiling = (uid: string, pid: string) => {
|
||||
try {
|
||||
HabbitApi.confirmProfiling(uid, pid).then((res) => {
|
||||
if (res) getProfilingData();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error confirming profiling:", error);
|
||||
message.error("Failed to confirm habit");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Preserved Habbit"
|
||||
open={open}
|
||||
width={800}
|
||||
footer={false}
|
||||
onCancel={onClose}
|
||||
maskClosable={false}
|
||||
>
|
||||
<div style={{ height: "50vh", overflow: "auto" }}>
|
||||
<div>
|
||||
Habbit enables Alias to learn user's preference and task specific
|
||||
best practices.
|
||||
</div>
|
||||
{/* Use extracted Header component */}
|
||||
<Header
|
||||
searchKeyword={searchKeyword}
|
||||
setSearchKeyword={setSearchKeyword}
|
||||
copyHandle={copyHandle}
|
||||
downHandle={downHandle}
|
||||
addHabbit={addHabbit}
|
||||
/>
|
||||
<List
|
||||
dataSource={filteredDataList}
|
||||
renderItem={(item) => (
|
||||
<List.Item className={styles.listItem}>
|
||||
<div className={styles.textEllipsis}>{item.content}</div>
|
||||
<div className={styles.icon}>
|
||||
<Flex align="center" gap="small">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "1",
|
||||
label: "Edit",
|
||||
icon: <SparkEditLine style={{ fontSize: 20 }} />,
|
||||
onClick: () => {
|
||||
editHabbit(item.content);
|
||||
setPid(item.pid);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Delete",
|
||||
danger: true,
|
||||
icon: <SparkDeleteLine style={{ fontSize: 20 }} />,
|
||||
onClick: () => {
|
||||
AlertDialog.warning({
|
||||
title: "Confirm deletion of this habbits?",
|
||||
children:
|
||||
"Once deleted, it cannot be recovered. Please proceed with caution.",
|
||||
centered: true,
|
||||
okText: "Confirm deletion",
|
||||
onOk: () => {
|
||||
deleteProfiling(item.uid, item.pid);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="middle"
|
||||
shape="default"
|
||||
className={styles.actionButtons}
|
||||
icon={<SparkMoreLine style={{ fontSize: 20 }} />}
|
||||
/>
|
||||
</Dropdown>
|
||||
{item.metadata.is_confirmed === 0 && (
|
||||
<Tooltip title="You need to manually click to confirm for it to take effect">
|
||||
<IconButton
|
||||
size="middle"
|
||||
shape="default"
|
||||
icon={
|
||||
<SparkUserCheckedLine style={{ fontSize: 20 }} />
|
||||
}
|
||||
onDoubleClick={() => {
|
||||
confirmProfiling(item.uid, item.pid);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={getTitle()}
|
||||
open={editOpen}
|
||||
width={700}
|
||||
okText="Sure"
|
||||
onCancel={onCloseEdit}
|
||||
maskClosable={false}
|
||||
onOk={onSure}
|
||||
footer={
|
||||
<Flex gap={16} align="center" justify="flex-end">
|
||||
<Button onClick={onCloseEdit}>Cancel</Button>
|
||||
<Button type="primary" loading={loading} onClick={onSure}>
|
||||
Sure
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
rows={Math.min(Math.max(3, habbitContent.split("\n").length + 1), 20)}
|
||||
onChange={(v) => {
|
||||
setHabbitContent(v.target.value || "");
|
||||
}}
|
||||
value={habbitContent}
|
||||
autoSize={{ minRows: 3, maxRows: 20 }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(HabbitModal);
|
||||
44
alias/frontend/src/pages/Chat/Prompts/index.module.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
.prompts {
|
||||
width: 100%;
|
||||
margin-top: 48px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
.header {
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
border-top: solid 1px var(--sps-color-border-secondary);
|
||||
}
|
||||
.promptsCard {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
padding: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.promptsIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.promptsName {
|
||||
font-weight: 500;
|
||||
}
|
||||
.promptsDesc {
|
||||
color: var(--sps-color-text-tertiary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.promptsButton {
|
||||
position: absolute;
|
||||
width: calc(100% - 60px);
|
||||
bottom: 22px;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
197
alias/frontend/src/pages/Chat/Prompts/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { originalPromptsList, promptJson } from "@/assets/json/prompt";
|
||||
import { RoadMapMessage } from "@/types/roadmap";
|
||||
import { ChatModeList, ChatModeType } from "@/utils/constant";
|
||||
import { Button, Card } from "@agentscope-ai/design";
|
||||
import {
|
||||
SparkAiEditLine,
|
||||
SparkAiNoteLine,
|
||||
SparkBehavioralInsightLine,
|
||||
SparkChatAnywhereLine,
|
||||
SparkMagicWandLine,
|
||||
SparkReplaceLine,
|
||||
SparkReplayLine,
|
||||
SparkRoboticsLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import { Col, Flex, Row } from "antd";
|
||||
import { debounce } from "lodash";
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import styles from "./index.module.scss";
|
||||
interface PromptsProps {
|
||||
handleSendMessage: (
|
||||
value?: string,
|
||||
description?: string,
|
||||
roadmap?: RoadMapMessage | null,
|
||||
files?: string | string[],
|
||||
) => void;
|
||||
chatMode: ChatModeType;
|
||||
}
|
||||
interface PromptsModeProps {
|
||||
title: string;
|
||||
describe: string;
|
||||
files?: string[] | string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const PromptCard: React.FC<{ item: PromptsModeProps }> = ({ item }) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
return (
|
||||
<Card
|
||||
className={styles.promptsCard}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Flex vertical gap={12}>
|
||||
{item.icon}
|
||||
<div className={styles.promptsName}>{item.title}</div>
|
||||
<div className={styles.promptsDesc}>{item.describe}</div>
|
||||
{isHovered && (
|
||||
<Button type="primary" className={styles.promptsButton}>
|
||||
Run Agent
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Icon array
|
||||
const icons = [
|
||||
<SparkReplayLine className={styles.promptsIcon} />,
|
||||
<SparkRoboticsLine className={styles.promptsIcon} />,
|
||||
<SparkMagicWandLine className={styles.promptsIcon} />,
|
||||
<SparkAiNoteLine className={styles.promptsIcon} />,
|
||||
<SparkAiEditLine className={styles.promptsIcon} />,
|
||||
<SparkChatAnywhereLine className={styles.promptsIcon} />,
|
||||
<SparkBehavioralInsightLine className={styles.promptsIcon} />,
|
||||
];
|
||||
|
||||
const Prompts: React.FC<PromptsProps> = ({ handleSendMessage, chatMode }) => {
|
||||
const fontSize = { fontSize: 18 };
|
||||
// Get corresponding prompt list from promptJson based on chatMode
|
||||
const modeSpecificPrompts = useMemo(() => {
|
||||
const currentMode = ChatModeList.find((mode) => mode.value === chatMode);
|
||||
const modeKey = currentMode?.value || ChatModeType.GENERAL;
|
||||
const promptsForMode: PromptsModeProps[] =
|
||||
promptJson[modeKey] || promptJson[ChatModeType.GENERAL] || [];
|
||||
return promptsForMode.length > 0 ? promptsForMode : originalPromptsList;
|
||||
}, [chatMode]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChangePrompts();
|
||||
}, [chatMode]);
|
||||
|
||||
// Function to generate random prompt items, ensuring no duplicates with previous ones
|
||||
const generateRandomPrompts = useCallback(
|
||||
(previousPrompts: any[] = []) => {
|
||||
// Get indices of previous prompt items
|
||||
const previousIndices = previousPrompts
|
||||
.map((prompt) =>
|
||||
modeSpecificPrompts.findIndex(
|
||||
(item: PromptsModeProps) => item.title === prompt.title,
|
||||
),
|
||||
)
|
||||
.filter((index) => index !== -1);
|
||||
|
||||
// Filter out unused prompt items
|
||||
const availableIndices = [
|
||||
...Array(modeSpecificPrompts.length).keys(),
|
||||
].filter((index) => !previousIndices.includes(index));
|
||||
|
||||
// Use all items if available items are less than 2
|
||||
const indicesToUse =
|
||||
availableIndices.length >= 2
|
||||
? availableIndices
|
||||
: [...Array(modeSpecificPrompts.length).keys()];
|
||||
|
||||
// Randomly select two different indices
|
||||
const shuffledIndices = indicesToUse
|
||||
.sort(() => 0.5 - Math.random())
|
||||
.slice(0, Math.min(2, modeSpecificPrompts.length)); // Ensure not exceeding available count
|
||||
|
||||
// Assign non-duplicate icons to each selected prompt
|
||||
const selectedIcons = [...icons];
|
||||
|
||||
return shuffledIndices.map((index) => {
|
||||
// Randomly select one from remaining icons
|
||||
const randomIconIndex = Math.floor(
|
||||
Math.random() * Math.min(selectedIcons.length, icons.length),
|
||||
);
|
||||
const icon = selectedIcons[randomIconIndex];
|
||||
|
||||
// Remove used icons from available icons to ensure no duplicates
|
||||
if (selectedIcons.length > 1) {
|
||||
selectedIcons.splice(randomIconIndex, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
...modeSpecificPrompts[index],
|
||||
icon,
|
||||
};
|
||||
});
|
||||
},
|
||||
[modeSpecificPrompts],
|
||||
);
|
||||
|
||||
// Use state to manage random prompt items
|
||||
const [randomPrompts, setRandomPrompts] = useState<PromptsModeProps[]>(() =>
|
||||
generateRandomPrompts(),
|
||||
);
|
||||
|
||||
// Function to update random prompt items
|
||||
const handleChangePrompts = useCallback(() => {
|
||||
setRandomPrompts((prevPrompts) => generateRandomPrompts(prevPrompts));
|
||||
}, [generateRandomPrompts]);
|
||||
|
||||
const debouncedHandleSendMessage = useCallback(
|
||||
debounce(
|
||||
(
|
||||
title: string,
|
||||
describe: string,
|
||||
roadmap: RoadMapMessage | null,
|
||||
files: string | string[] | undefined,
|
||||
) => {
|
||||
handleSendMessage(title, describe, roadmap, files);
|
||||
},
|
||||
300,
|
||||
),
|
||||
[handleSendMessage],
|
||||
);
|
||||
return (
|
||||
<div className={styles.prompts}>
|
||||
<Flex align="center" justify="space-between" className={styles.header}>
|
||||
<div>Starting from these cases</div>
|
||||
<Flex
|
||||
gap="middle"
|
||||
onClick={handleChangePrompts}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<SparkReplaceLine style={fontSize} />
|
||||
Change it
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Row gutter={[16, 16]}>
|
||||
{randomPrompts.map((item, index) => {
|
||||
return (
|
||||
<Col
|
||||
span={12}
|
||||
key={item.title}
|
||||
onClick={() => {
|
||||
if (item.title)
|
||||
debouncedHandleSendMessage(
|
||||
item.title,
|
||||
item.describe,
|
||||
null,
|
||||
item.files,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PromptCard item={item} />
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Prompts);
|
||||
50
alias/frontend/src/pages/Chat/RoadmapButton/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { memo, useRef, useLayoutEffect } from "react";
|
||||
import { Button } from "@agentscope-ai/design";
|
||||
import { Flex } from "antd";
|
||||
import { SparkEditLine } from "@agentscope-ai/icons";
|
||||
import { useWorkspace } from "@/context/WorkspaceContext";
|
||||
import { isAtBottom } from "@/utils/sharedRefs";
|
||||
|
||||
interface RoadmapButtonProps {
|
||||
handleSendMessage: (message: string) => void;
|
||||
setShow: (show: boolean) => void;
|
||||
startTimer: () => void;
|
||||
setShowRoadmapEditBtn: (edit: boolean) => void;
|
||||
}
|
||||
const RoadmapButton: React.FC<RoadmapButtonProps> = ({
|
||||
handleSendMessage,
|
||||
setShow,
|
||||
startTimer,
|
||||
setShowRoadmapEditBtn,
|
||||
}) => {
|
||||
const { setActiveKey } = useWorkspace();
|
||||
const shouldScrollRef = useRef(isAtBottom.current);
|
||||
useLayoutEffect(() => {
|
||||
if (!shouldScrollRef.current) return;
|
||||
startTimer();
|
||||
}, [startTimer]);
|
||||
const acceptedRoadmap = () => {
|
||||
const acceptedMessage =
|
||||
"I have accepted the Roadmap, please proceed with the execution";
|
||||
handleSendMessage(acceptedMessage);
|
||||
setShow(false);
|
||||
};
|
||||
return (
|
||||
<Flex gap={16} align="center" style={{ paddingBottom: 80 }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveKey("roadmap");
|
||||
setShow(false);
|
||||
setShowRoadmapEditBtn(true);
|
||||
}}
|
||||
>
|
||||
<SparkEditLine /> Edit Roadmap
|
||||
</Button>
|
||||
<Button onClick={acceptedRoadmap}>
|
||||
<SparkEditLine /> Accept Roadmap
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RoadmapButton);
|
||||
172
alias/frontend/src/pages/Chat/Sidebar/index.module.scss
Normal file
@@ -0,0 +1,172 @@
|
||||
.sidebarWrapper {
|
||||
@apply h-full max-h-screen relative flex flex-col;
|
||||
width: 72px;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-right: 1px solid var(--sps-color-border-secondary);
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.expanded {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@apply w-full h-full flex flex-col items-center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sidebarLogo {
|
||||
@apply flex justify-between items-center space-x-2;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
/* Explicit height to match mini sidebar item/logo area */
|
||||
margin-bottom: 8px;
|
||||
/* Consistent margin */
|
||||
}
|
||||
|
||||
// @apply mb-8 transition-transform hover:scale-105;
|
||||
.sidebarNav {
|
||||
@apply flex-1 flex flex-col items-center;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
/* Hide scrollbar but still scrollable */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* Firefox */
|
||||
.active {
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarItem {
|
||||
@apply p-2 flex items-center space-x-2 mt-1 cursor-pointer rounded-md font-medium;
|
||||
height: 40px;
|
||||
/* Fixed height 40px */
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease-in-out;
|
||||
/* Animate padding and other props */
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
}
|
||||
|
||||
.itemText {
|
||||
transition: opacity 0.2s ease-in-out, width 0.2s ease-in-out, margin 0.2s ease-in-out;
|
||||
opacity: 1;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
.sidebarItem {
|
||||
padding-left: 10px;
|
||||
/* Center 20px icon in 40px width (10+20+10) */
|
||||
padding-right: 10px;
|
||||
|
||||
.itemText {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
margin-left: 0;
|
||||
/* Remove space-x-2 margin */
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarNav {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.historyPopover {
|
||||
:global {
|
||||
.ant-popover-inner {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.agent-scope-chat-history-panel-item {
|
||||
margin: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sps-color-fill-tertiary);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-scope-chat-history-panel,
|
||||
.agent-scope-chat-history-panel-list,
|
||||
[class*="agent-scope-chat-history"] {
|
||||
gap: 0 !important;
|
||||
row-gap: 0 !important;
|
||||
column-gap: 0 !important;
|
||||
}
|
||||
|
||||
// Remove all possible spacing
|
||||
* {
|
||||
&[class*="agent-scope-chat-history-panel-item"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popoverContent {
|
||||
width: 240px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
// Ensure HistoryPanel has no extra spacing
|
||||
>* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popoverHeader {
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
/* No border-bottom as requested */
|
||||
}
|
||||
|
||||
.siderBarHistory {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sidebarFooter {
|
||||
@apply mt-auto flex flex-col items-center gap-4 pt-4;
|
||||
width: 100%;
|
||||
|
||||
.footerBox {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
419
alias/frontend/src/pages/Chat/Sidebar/index.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
import LogoIcon from "@/components/LogoIcon";
|
||||
import { useMessage } from "@/context/MessageContext";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { useWorkspace } from "@/context/WorkspaceContext";
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import { loginApi } from "@/services/api/login";
|
||||
import type { ApiResponse, ListResponsePayload } from "@/types/api";
|
||||
import { Conversation } from "@/types/api";
|
||||
import { HistoryPanel } from "@agentscope-ai/chat";
|
||||
import { AlertDialog, Button, message, Popover } from "@agentscope-ai/design";
|
||||
import {
|
||||
SparkChatTabFill,
|
||||
SparkDeleteLine,
|
||||
SparkDownLine,
|
||||
SparkEffciencyLine,
|
||||
SparkHistoryLine,
|
||||
SparkMessageLine,
|
||||
SparkMoonLine,
|
||||
SparkOperateLeftLine,
|
||||
SparkOperateRightLine,
|
||||
SparkSocialInteraction01Line,
|
||||
SparkSunLine,
|
||||
SparkUpLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import classNames from "classnames";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Avatar from "../Avatar";
|
||||
import HabbitModal from "../Habbit";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface SidebarProps {
|
||||
conversationId: string;
|
||||
onSelectConversation: (conversation: Conversation) => void;
|
||||
setNewConversation: () => void;
|
||||
}
|
||||
|
||||
// History type
|
||||
type HistoryType = "collected" | "all";
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
conversationId,
|
||||
onSelectConversation,
|
||||
setNewConversation,
|
||||
}) => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { setDisplayedContent, setActiveKey, setArgs, setMessageList } =
|
||||
useWorkspace();
|
||||
const { setCurrentConversation } = useMessage();
|
||||
const navigate = useNavigate();
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [showList, setShowList] = useState<boolean>(true);
|
||||
const [showCollect, setShowCollect] = useState<boolean>(true);
|
||||
const [showConversationList, setShowConversationList] = useState(true);
|
||||
const [enterLogo, setEnterLogo] = useState<boolean>(false);
|
||||
const [showHabbit, setShowHabbit] = useState<boolean>(false);
|
||||
const [userInfo, setUserInfo] = useState({
|
||||
uid: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
});
|
||||
const iconStyle = { fontSize: 20 };
|
||||
const isHasFetchedRef = useRef(false);
|
||||
|
||||
const fetchConversations = async () => {
|
||||
try {
|
||||
const response = await conversationApi.list({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
order_by: "create_time",
|
||||
order_direction: "desc",
|
||||
});
|
||||
if (response.status && response.payload) {
|
||||
const listResponse = response as unknown as ApiResponse<
|
||||
ListResponsePayload<Conversation>
|
||||
>;
|
||||
// Ensure structure matches expected format before accessing
|
||||
const payloadItems = listResponse.payload?.items;
|
||||
if (Array.isArray(payloadItems)) {
|
||||
setConversations(payloadItems);
|
||||
} else {
|
||||
setConversations([]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Failed to fetch conversation history");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHasFetchedRef.current) {
|
||||
loginApi.getUsers().then((res) => {
|
||||
setUserInfo({
|
||||
uid: res?.payload?.id || "",
|
||||
userName: res?.payload?.username || "",
|
||||
email: res?.payload?.email || "",
|
||||
});
|
||||
});
|
||||
isHasFetchedRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConversations();
|
||||
}, [showConversationList]);
|
||||
|
||||
const clearWorkspace = () => {
|
||||
setDisplayedContent(null);
|
||||
setArgs({});
|
||||
setActiveKey("1");
|
||||
setMessageList([]);
|
||||
};
|
||||
|
||||
const hideHistory = () => {
|
||||
setShowConversationList(false);
|
||||
onMouseOutLogo();
|
||||
};
|
||||
|
||||
const goChatDetails = (convId: string) => {
|
||||
if (convId === conversationId) return;
|
||||
clearWorkspace();
|
||||
const conv = conversations.find((item) => item.id === convId);
|
||||
if (conv) {
|
||||
onSelectConversation(conv);
|
||||
setCurrentConversation(conv);
|
||||
hideHistory();
|
||||
}
|
||||
};
|
||||
|
||||
const goNewChat = () => {
|
||||
onMouseOutLogo();
|
||||
clearWorkspace();
|
||||
setNewConversation();
|
||||
setShowConversationList(false);
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
onMouseOutLogo();
|
||||
clearWorkspace();
|
||||
setNewConversation();
|
||||
setCurrentConversation(null);
|
||||
navigate("/");
|
||||
setShowConversationList(false);
|
||||
};
|
||||
|
||||
const deletedConversation = async (id: string) => {
|
||||
AlertDialog.warning({
|
||||
title: "Confirm deletion of this conversation?",
|
||||
children:
|
||||
"Once deleted, it cannot be recovered. Please proceed with caution.",
|
||||
centered: true,
|
||||
okText: "Confirm deletion",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const reslut = await conversationApi.delete(id);
|
||||
if (reslut.status) {
|
||||
setConversations(
|
||||
conversations.filter((item) => item.id !== id) || [],
|
||||
);
|
||||
message.success(
|
||||
reslut.message || "Conversation deleted successfully",
|
||||
);
|
||||
if (conversationId === id) goNewChat();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error("Failed to delete conversation");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseEnterLogo = () => {
|
||||
setEnterLogo(true);
|
||||
};
|
||||
|
||||
const onMouseOutLogo = () => {
|
||||
setEnterLogo(false);
|
||||
};
|
||||
|
||||
const expandSidebar = () => {
|
||||
if (!conversationId) {
|
||||
setShowConversationList(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Get delete menu item
|
||||
const getDeleteMenuItem = (id: string) => ({
|
||||
label: "Delete",
|
||||
key: "delete",
|
||||
icon: <SparkDeleteLine />,
|
||||
danger: true,
|
||||
onClick: () => deletedConversation(id),
|
||||
});
|
||||
|
||||
// Get filtered conversation list
|
||||
const getFilteredConversations = (type: HistoryType) => {
|
||||
if (type === "collected") {
|
||||
return conversations.filter((conv) => conv.collected);
|
||||
}
|
||||
return conversations.filter((conv) => !conv.collected);
|
||||
};
|
||||
|
||||
// Render history panel
|
||||
const renderHistoryPanel = (type: HistoryType, isExpanded: boolean) => {
|
||||
const isCollected = type === "collected";
|
||||
const filteredConversations = getFilteredConversations(type);
|
||||
const showPanel = isCollected ? showCollect : showList;
|
||||
const setShowPanel = isCollected ? setShowCollect : setShowList;
|
||||
const title = isCollected ? "My Collection" : "History";
|
||||
const icon = isCollected ? (
|
||||
<SparkSocialInteraction01Line style={iconStyle} />
|
||||
) : (
|
||||
<SparkHistoryLine style={iconStyle} />
|
||||
);
|
||||
|
||||
// Don't show collection section if there are no collected conversations
|
||||
if (isCollected && filteredConversations.length === 0) return null;
|
||||
|
||||
// Show empty state if there are no conversations (only when expanded)
|
||||
if (filteredConversations.length === 0) {
|
||||
return isExpanded && !isCollected ? (
|
||||
<div>No conversation records</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.siderBarHistory}>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<div
|
||||
className={styles.sidebarItem}
|
||||
onClick={() => setShowPanel(!showPanel)}
|
||||
>
|
||||
{icon}
|
||||
<div className={styles.itemText}>{title}</div>
|
||||
{showPanel ? <SparkDownLine /> : <SparkUpLine />}
|
||||
</div>
|
||||
|
||||
{showPanel && (
|
||||
<HistoryPanel
|
||||
menu={(session) => ({
|
||||
items: [getDeleteMenuItem(session.key)],
|
||||
})}
|
||||
items={filteredConversations.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
onActiveChange={goChatDetails}
|
||||
activeKey={conversationId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Popover
|
||||
placement="rightTop"
|
||||
trigger="hover"
|
||||
overlayClassName={styles.historyPopover}
|
||||
onOpenChange={(open) => open && fetchConversations()}
|
||||
content={
|
||||
<div className={styles.popoverContent}>
|
||||
<div className={styles.popoverHeader}>{title}</div>
|
||||
<HistoryPanel
|
||||
menu={(session) => ({
|
||||
items: [getDeleteMenuItem(session.key)],
|
||||
})}
|
||||
items={filteredConversations.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
onActiveChange={goChatDetails}
|
||||
activeKey={conversationId}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.sidebarItem} title={title}>
|
||||
{icon}
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const isExpanded = !conversationId && showConversationList;
|
||||
const isHomePage = !conversationId;
|
||||
|
||||
const renderSidebarContent = () => (
|
||||
<nav
|
||||
className={classNames(styles.sidebar, {
|
||||
[styles.collapsed]: !isExpanded,
|
||||
})}
|
||||
>
|
||||
{/* Logo Section */}
|
||||
<div className={styles.sidebarLogo}>
|
||||
<div
|
||||
className={classNames(
|
||||
"transition-transform hover:scale-105 w-10 h-10 cursor-pointer relative",
|
||||
)}
|
||||
onClick={conversationId ? goHome : expandSidebar}
|
||||
onMouseEnter={isHomePage ? onMouseEnterLogo : undefined}
|
||||
onMouseLeave={isHomePage ? onMouseOutLogo : undefined}
|
||||
>
|
||||
<LogoIcon
|
||||
className={classNames(
|
||||
"w-full h-full object-cover transition-opacity",
|
||||
{
|
||||
"opacity-0": isHomePage && !isExpanded && enterLogo,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
{/* When on homepage and not expanded, show expand icon on hover, overlay on logo */}
|
||||
{isHomePage && !isExpanded && enterLogo && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center cursor-pointer transition-transform hover:scale-105 z-10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
expandSidebar();
|
||||
}}
|
||||
>
|
||||
<SparkOperateRightLine style={iconStyle} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div
|
||||
className="w-5 h-5 cursor-pointer transition-transform hover:scale-105"
|
||||
onClick={hideHistory}
|
||||
>
|
||||
<SparkOperateLeftLine style={iconStyle} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Section */}
|
||||
<div className={styles.sidebarNav}>
|
||||
<div
|
||||
className={classNames(styles.sidebarItem, {
|
||||
[styles.active]: !conversationId,
|
||||
})}
|
||||
onClick={goNewChat}
|
||||
title={!isExpanded ? "New Agent Task" : undefined}
|
||||
>
|
||||
{isHomePage ? (
|
||||
<SparkChatTabFill style={iconStyle} />
|
||||
) : (
|
||||
<SparkMessageLine style={iconStyle} />
|
||||
)}
|
||||
<div className={styles.itemText}>New Agent Task</div>
|
||||
</div>
|
||||
|
||||
{/* History Sections */}
|
||||
{renderHistoryPanel("collected", isExpanded)}
|
||||
{renderHistoryPanel("all", isExpanded)}
|
||||
</div>
|
||||
|
||||
{/* Footer Section */}
|
||||
<div className={styles.sidebarFooter}>
|
||||
{isExpanded ? (
|
||||
<div className={styles.footerBox}>
|
||||
<div className={styles.footerLeft}>
|
||||
<Avatar userInfo={userInfo} />
|
||||
<div className="ml-2">{userInfo.userName}</div>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<SparkMoonLine style={iconStyle} />
|
||||
) : (
|
||||
<SparkSunLine style={iconStyle} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => setShowHabbit(true)}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
<SparkEffciencyLine style={iconStyle} />
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className={styles.iconButton}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<SparkMoonLine style={iconStyle} />
|
||||
) : (
|
||||
<SparkSunLine style={iconStyle} />
|
||||
)}
|
||||
</Button>
|
||||
<Avatar userInfo={userInfo} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.sidebarWrapper, {
|
||||
[styles.expanded]: isExpanded,
|
||||
})}
|
||||
>
|
||||
{renderSidebarContent()}
|
||||
<HabbitModal
|
||||
open={showHabbit}
|
||||
setOpen={setShowHabbit}
|
||||
uid={userInfo.uid}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Sidebar);
|
||||
41
alias/frontend/src/pages/Chat/WelcomeView/index.module.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
.welcome {
|
||||
// margin-top: 24px;
|
||||
.githubButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
.button {
|
||||
width: 180px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
letter-spacing: normal;
|
||||
color: var(--sps-color-text);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
.logo {
|
||||
height: 72px;
|
||||
width: 80px;
|
||||
margin: 0 12px;
|
||||
margin-top: -12px;
|
||||
}
|
||||
.label {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 28px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
letter-spacing: normal;
|
||||
color: var(--sps-color-text-tertiary);
|
||||
}
|
||||
}
|
||||
59
alias/frontend/src/pages/Chat/WelcomeView/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { memo } from "react";
|
||||
import { Button, Flex } from "antd";
|
||||
import { Welcome } from "@agentscope-ai/chat";
|
||||
import { SparkUpperrightArrowLine } from "@agentscope-ai/icons";
|
||||
import LogoIcon from "@/components/LogoIcon";
|
||||
import styles from "./index.module.scss";
|
||||
const WelcomeView: React.FC = ({}) => {
|
||||
const goGitHub = (url: string) => {
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
return (
|
||||
<Flex vertical className={styles.welcome}>
|
||||
<div className={styles.githubButton}>
|
||||
<Flex gap="small">
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
goGitHub("https://github.com/agentscope-ai/agentscope");
|
||||
}}
|
||||
>
|
||||
AgentScope Github
|
||||
<SparkUpperrightArrowLine style={{ fontSize: "20px" }} />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
goGitHub(
|
||||
"https://github.com/agentscope-ai/agentscope-samples/tree/main/alias",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Alias Github
|
||||
<SparkUpperrightArrowLine style={{ fontSize: "20px" }} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<Welcome
|
||||
style={{ justifyContent: "center", flex: 1 }}
|
||||
logo={null}
|
||||
title={
|
||||
<div className={styles.title}>
|
||||
<div className={styles.label}>Tell</div>
|
||||
<div className={styles.logo}>
|
||||
<LogoIcon className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className={styles.label}> what you want to do</div>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
<div className={styles.description}>
|
||||
Let the Agent help you with everything you want to do
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WelcomeView);
|
||||
70
alias/frontend/src/pages/Chat/hooks/useConversationState.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||
export const useConversationState = () => {
|
||||
const [conversationStates, setConversationStates] = useState<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
isGenerating: boolean;
|
||||
taskId: string;
|
||||
isThinking: boolean;
|
||||
}
|
||||
>
|
||||
>({});
|
||||
|
||||
const updateConversationState = useCallback(
|
||||
(
|
||||
conversationId: string,
|
||||
state: Partial<{
|
||||
isGenerating: boolean;
|
||||
taskId: string;
|
||||
isThinking: boolean;
|
||||
}>,
|
||||
) => {
|
||||
if (!conversationId) return;
|
||||
|
||||
setConversationStates((prev) => ({
|
||||
...prev,
|
||||
[conversationId]: {
|
||||
isGenerating:
|
||||
state.isGenerating ?? prev[conversationId]?.isGenerating ?? false,
|
||||
taskId: state.taskId ?? prev[conversationId]?.taskId ?? "",
|
||||
isThinking:
|
||||
state.isThinking ?? prev[conversationId]?.isThinking ?? false,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getConversationState = useCallback(
|
||||
(conversationId: string) => {
|
||||
if (!conversationId)
|
||||
return { isGenerating: false, taskId: "", isThinking: false };
|
||||
return (
|
||||
conversationStates[conversationId] || {
|
||||
isGenerating: false,
|
||||
taskId: "",
|
||||
isThinking: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
[conversationStates],
|
||||
);
|
||||
|
||||
const clearConversationState = useCallback((conversationId: string) => {
|
||||
if (!conversationId) return;
|
||||
|
||||
setConversationStates((prev) => {
|
||||
const newState = { ...prev };
|
||||
delete newState[conversationId];
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
conversationStates,
|
||||
updateConversationState,
|
||||
getConversationState,
|
||||
clearConversationState,
|
||||
};
|
||||
};
|
||||
156
alias/frontend/src/pages/Chat/index.module.scss
Normal file
@@ -0,0 +1,156 @@
|
||||
.home {
|
||||
@apply h-screen w-full flex overflow-hidden bg-page-bg;
|
||||
// > div {
|
||||
// @apply h-full w-full flex overflow-hidden;
|
||||
// }
|
||||
}
|
||||
|
||||
/* Right content container */
|
||||
.newContainer {
|
||||
@apply flex-1 flex justify-center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Chat area */
|
||||
.newChatSection {
|
||||
@apply flex-1 rounded-[24px];
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
min-width: 300px;
|
||||
max-width: 800px;
|
||||
// position: relative;
|
||||
margin: auto;
|
||||
|
||||
/* Disclaimer at bottom */
|
||||
:global(.sps-disclaimer) {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right content container */
|
||||
.rightContents {
|
||||
width: calc(100vw - 72px);
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply flex-1 flex min-w-0;
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
|
||||
/* Chat area */
|
||||
.chatSection {
|
||||
@apply w-2/5 flex flex-col rounded-[24px] pl-4;
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
min-width: 300px;
|
||||
|
||||
.chatContent {
|
||||
@apply flex-1 flex flex-col h-full relative;
|
||||
}
|
||||
|
||||
.messageArea {
|
||||
@apply flex-1 overflow-y-auto scroll-smooth;
|
||||
@apply scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent;
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&.userMessage {
|
||||
flex-direction: row-reverse;
|
||||
margin-right: 1.5rem;
|
||||
|
||||
.messageText {
|
||||
margin-right: 1rem;
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border-radius: 1rem 0.25rem 1rem 1rem;
|
||||
|
||||
.fileAttachment {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.fileInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
img {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@apply w-8 h-8 flex-shrink-0 mr-3;
|
||||
|
||||
img {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
}
|
||||
|
||||
.messageText {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem 1rem 1rem 1rem;
|
||||
background-color: var(--message-option-bg-hover);
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.optionsContainer {
|
||||
@apply bg-page-container rounded-[20px];
|
||||
width: calc(80% + 2.75rem);
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply font-pingfang text-[14px] leading-[20px] text-text-secondary mb-[9px] px-[30px] pt-[10px];
|
||||
}
|
||||
|
||||
.optionsList {
|
||||
@apply space-y-[3px] pl-[12px] pr-[64px] pb-[10px];
|
||||
}
|
||||
|
||||
.optionButton {
|
||||
@apply block w-full py-[10px] rounded-[12px] bg-white text-left px-[30px] border border-border-option transition-all duration-200;
|
||||
@apply hover:border-purple-200 hover:shadow-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace area */
|
||||
.workspaceSection {
|
||||
@apply w-3/5 flex flex-col rounded-[8px] ml-4 mr-4;
|
||||
@apply transition-all duration-300 ease-in-out;
|
||||
@apply relative pb-[12px];
|
||||
background-color: var(--sps-color-bg-base);
|
||||
min-width: 300px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--sps-color-border-secondary);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tabSpace {
|
||||
:global {
|
||||
.css-var-r1.sps-segmented {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
623
alias/frontend/src/pages/Chat/index.tsx
Normal file
@@ -0,0 +1,623 @@
|
||||
import { MessageList } from "@/components/Chat/MessageList";
|
||||
import LoginModal from "@/components/LoginModal";
|
||||
import Roadmap from "@/components/Roadmap";
|
||||
import SandBox from "@/components/SandBox";
|
||||
import ScrollToBottomButton from "@/components/ScrollToBottomButton";
|
||||
import Workspace from "@/components/Workspace";
|
||||
import { useMessage } from "@/context/MessageContext";
|
||||
import { useWorkspace } from "@/context/WorkspaceContext.tsx";
|
||||
import { conversationApi } from "@/services/api/conversation";
|
||||
import {
|
||||
ApiMessage,
|
||||
ApiResponse,
|
||||
Conversation,
|
||||
FilePreview as FilePreviewProps,
|
||||
ListResponsePayload,
|
||||
SSEResponse,
|
||||
} from "@/types/api";
|
||||
import {
|
||||
Message,
|
||||
MessageRole,
|
||||
MessageState,
|
||||
MessageType,
|
||||
} from "@/types/message";
|
||||
import { RoadMapDataProps, RoadMapMessage, RoadMapType } from "@/types/roadmap";
|
||||
import { ChatModeType, LANGUAGETYPE } from "@/utils/constant";
|
||||
import { isAtBottom } from "@/utils/sharedRefs";
|
||||
import { message, Tabs, type TabsProps } from "@agentscope-ai/design";
|
||||
import useUrlState from "@ahooksjs/use-url-state";
|
||||
import dayjs from "dayjs";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import ChatHeader from "./ChatHeader";
|
||||
import ChatInput from "./ChatInput";
|
||||
import ChatMode from "./ChatMode";
|
||||
import styles from "./index.module.scss";
|
||||
import Prompts from "./Prompts";
|
||||
import RoadmapButton from "./RoadmapButton";
|
||||
import Sidebar from "./Sidebar";
|
||||
import { mapApiMessageToChatMessage, getPromptFile } from "./utils";
|
||||
import WelcomeView from "./WelcomeView";
|
||||
|
||||
export const Chat = () => {
|
||||
const [state, setState] = useUrlState({ conversationId: "" });
|
||||
const { conversationId } = state;
|
||||
const { setCurrentConversation, currentConversation } = useMessage();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const { activeKey, setActiveKey } = useWorkspace();
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [filePreview, setFilePreview] = useState<FilePreviewProps[]>([]);
|
||||
const [roadmapData, setRoadmapData] = useState<RoadMapDataProps | null>(null);
|
||||
const [isThinking, setIsThinking] = useState(false);
|
||||
const [thinkingConversationId, setThinkingConversationId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [chatMode, setChatMode] = useState<ChatModeType>(ChatModeType.GENERAL);
|
||||
const roadmapUpdateQuery = "I have updated the task content";
|
||||
const ScrollToBottomButtonRef = useRef<any>(null);
|
||||
const [taskId, setTaskId] = useState(""); // task_id is needed to stop conversation
|
||||
const [languageType, setLanguageType] = useState<string>(LANGUAGETYPE.en_US);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [showRoadmapSelectBtn, setShowRoadmapSelectBtn] = useState(false); // Show roadmap button
|
||||
const [showRoadmapEditBtn, setShowRoadmapEditBtn] = useState(false);
|
||||
// Cancel ongoing requests when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// setOnDoneCallback(async (conversationId, taskId, messageId) => {
|
||||
// });
|
||||
if (
|
||||
roadmapData &&
|
||||
roadmapData?.subtasks &&
|
||||
Array.isArray(roadmapData?.subtasks)
|
||||
) {
|
||||
const subtasks = roadmapData?.subtasks.filter(
|
||||
(item) => item.state === RoadMapType.TODO,
|
||||
);
|
||||
if (
|
||||
subtasks.length > 0 &&
|
||||
subtasks.length === roadmapData?.subtasks?.length &&
|
||||
!isGenerating &&
|
||||
!isThinking
|
||||
) {
|
||||
setShowRoadmapSelectBtn(true);
|
||||
} else {
|
||||
setShowRoadmapSelectBtn(false);
|
||||
}
|
||||
}
|
||||
}, [roadmapData]);
|
||||
|
||||
// Handle roadmap data updates
|
||||
useEffect(() => {
|
||||
const handleRoadmapUpdate = (message: any) => {
|
||||
if (
|
||||
message.type === MessageType.RESPONSE &&
|
||||
typeof message.content === "object" &&
|
||||
message.content !== null &&
|
||||
"data" in message.content &&
|
||||
message.content.data?.roadmap &&
|
||||
Object.keys(message.content?.data?.roadmap).length > 0
|
||||
) {
|
||||
setRoadmapData(message.content.data.roadmap as RoadMapDataProps);
|
||||
}
|
||||
};
|
||||
|
||||
if (messages) {
|
||||
messages.forEach(handleRoadmapUpdate);
|
||||
}
|
||||
}, [messages]);
|
||||
useEffect(() => {
|
||||
const loadConversationFromStorage = async () => {
|
||||
if (conversationId) {
|
||||
await handleGetMessages(conversationId);
|
||||
await handleGetRoadmap(conversationId);
|
||||
await handleGetCurConversation(conversationId);
|
||||
}
|
||||
};
|
||||
loadConversationFromStorage();
|
||||
}, []);
|
||||
const createNewConversation = async (
|
||||
name: string,
|
||||
description: string,
|
||||
chatMode: string,
|
||||
) => {
|
||||
try {
|
||||
const defaultName = `Conversation ${dayjs().format("YYYY-MM-DD HH:mm")}`;
|
||||
const response = await conversationApi.create(
|
||||
name || defaultName,
|
||||
description || "empty",
|
||||
chatMode,
|
||||
);
|
||||
if (!response.status || !response.payload) {
|
||||
throw new Error("Failed to create conversation");
|
||||
}
|
||||
const targetConversationId = response.payload.id;
|
||||
const newConversation: Conversation = {
|
||||
id: targetConversationId,
|
||||
name: name || defaultName,
|
||||
description: description || "empty",
|
||||
runtime_status: "running",
|
||||
create_time: new Date().toISOString(),
|
||||
update_time: new Date().toISOString(),
|
||||
user_id: response.payload.user_id,
|
||||
artifacts_sio: response?.payload?.artifacts_sio || "",
|
||||
browser_ws: response?.payload?.browser_ws || "",
|
||||
runtime_token: response?.payload?.runtime_token || "",
|
||||
shared: response?.payload?.shared || false,
|
||||
sandbox_url: response?.payload?.sandbox_url || "",
|
||||
chat_mode: response?.payload?.chat_mode || ChatModeType.GENERAL,
|
||||
};
|
||||
setCurrentConversation(newConversation);
|
||||
setChatMode(response?.payload?.chat_mode || ChatModeType.GENERAL);
|
||||
return targetConversationId;
|
||||
} catch (error) {
|
||||
console.error("Failed to create conversation:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const startTimer = useCallback(() => {
|
||||
if (ScrollToBottomButtonRef?.current) {
|
||||
ScrollToBottomButtonRef.current.scrollToBottom("auto");
|
||||
}
|
||||
}, []);
|
||||
const resetThinkingState = () => {
|
||||
setIsThinking(false);
|
||||
setThinkingConversationId(null);
|
||||
};
|
||||
const sendMessageWithHandling = async (
|
||||
conversationId: string,
|
||||
content: string,
|
||||
parentMessageId: string,
|
||||
fileIds?: string[],
|
||||
roadmap?: RoadMapMessage | null,
|
||||
) => {
|
||||
setIsThinking(true);
|
||||
setThinkingConversationId(conversationId);
|
||||
// Create new AbortController
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
try {
|
||||
await conversationApi.sendMessage(
|
||||
conversationId,
|
||||
content,
|
||||
(parsed: SSEResponse) => {
|
||||
if (parsed.data.messages && parsed.data.messages.length > 0) {
|
||||
setMessages((prevMessages) => {
|
||||
const newMessages = parsed.data.messages.map((msg) => {
|
||||
const chatMessage = mapApiMessageToChatMessage(msg, true);
|
||||
return {
|
||||
...chatMessage,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
});
|
||||
const updatedMessages = prevMessages.slice();
|
||||
newMessages.forEach((newMsg) => {
|
||||
const index = updatedMessages.findIndex(
|
||||
(existingMsg) => existingMsg.id === newMsg.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
updatedMessages[index] = newMsg;
|
||||
} else {
|
||||
updatedMessages.push(newMsg);
|
||||
}
|
||||
});
|
||||
return updatedMessages;
|
||||
});
|
||||
resetThinkingState();
|
||||
if (parsed?.task_id) setTaskId(parsed.task_id);
|
||||
}
|
||||
// Can stop message if messages is empty array and has task_id
|
||||
if (
|
||||
parsed.data.messages &&
|
||||
parsed.data.messages.length === 0 &&
|
||||
parsed?.task_id
|
||||
) {
|
||||
setTaskId(parsed.task_id);
|
||||
}
|
||||
if (
|
||||
parsed.data.roadmap &&
|
||||
Object.keys(parsed.data.roadmap).length > 0
|
||||
) {
|
||||
setRoadmapData(parsed.data.roadmap);
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
// Don't show error message if it's a cancel operation
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
resetThinkingState();
|
||||
return;
|
||||
}
|
||||
message.error({
|
||||
content: error.message || "Request failed, please try again later",
|
||||
duration: 3,
|
||||
});
|
||||
setTimeout(() => {
|
||||
resetThinkingState();
|
||||
}, 1000);
|
||||
throw error;
|
||||
},
|
||||
"task",
|
||||
fileIds ? fileIds : [],
|
||||
languageType,
|
||||
chatMode,
|
||||
abortController,
|
||||
roadmap,
|
||||
);
|
||||
} catch (error: any) {
|
||||
// Don't show error message if it's a cancel operation
|
||||
if (error.name === "AbortError" || abortController.signal.aborted) {
|
||||
resetThinkingState();
|
||||
return;
|
||||
}
|
||||
console.error("API call failed:", error);
|
||||
// Remove thinking state on error
|
||||
resetThinkingState();
|
||||
|
||||
const errorMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: MessageRole.ASSISTANT,
|
||||
content: "Error, please try again later.",
|
||||
status: MessageState.ERROR,
|
||||
create_time: new Date().toISOString(),
|
||||
update_time: new Date().toISOString(),
|
||||
feedback: null,
|
||||
conversation_id: conversationId,
|
||||
parent_message_id: parentMessageId,
|
||||
type: MessageType.RESPONSE,
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
throw error; // Rethrow error for further handling
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setMessages((prevMessages) => {
|
||||
return prevMessages.map((message) => ({
|
||||
...message,
|
||||
isGenerating: false,
|
||||
}));
|
||||
});
|
||||
// Clean up AbortController reference
|
||||
if (abortControllerRef.current === abortController) {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle option selection
|
||||
const handleClarificationSelect = async (options: string[]) => {
|
||||
if (isGenerating || !conversationId) return;
|
||||
await handleSendMessage(options.join(", "));
|
||||
};
|
||||
|
||||
const handleGetMessages = async (conversationId: string) => {
|
||||
try {
|
||||
const response = await conversationApi.getMessages(conversationId);
|
||||
if (response.status && response.payload) {
|
||||
const apiResponse = response as unknown as ApiResponse<
|
||||
ListResponsePayload<ApiMessage>
|
||||
>;
|
||||
const messages = apiResponse.payload.items;
|
||||
if (messages && messages.length > 0) {
|
||||
const historyMessages = messages
|
||||
.filter((msg) => msg.message.content !== null)
|
||||
.map((msg) => mapApiMessageToChatMessage(msg));
|
||||
|
||||
historyMessages.sort(
|
||||
(a, b) =>
|
||||
new Date(a.create_time).getTime() -
|
||||
new Date(b.create_time).getTime(),
|
||||
);
|
||||
setMessages(historyMessages);
|
||||
} else {
|
||||
setMessages([]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load conversation messages:", error);
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetRoadmap = async (conversationId: string) => {
|
||||
try {
|
||||
const roadmap_response = await conversationApi.getRoadmap(conversationId);
|
||||
if (roadmap_response.status && roadmap_response.payload) {
|
||||
const roadmap = (
|
||||
roadmap_response as unknown as ApiResponse<RoadMapDataProps>
|
||||
).payload;
|
||||
if (roadmap) {
|
||||
setRoadmapData(roadmap);
|
||||
} else {
|
||||
setRoadmapData(null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load conversation roadmap:", error);
|
||||
setRoadmapData(null);
|
||||
}
|
||||
};
|
||||
const handleGetCurConversation = async (conversationId: string) => {
|
||||
try {
|
||||
const response = await conversationApi.getConversation(conversationId);
|
||||
if (response.status && response.payload) {
|
||||
setCurrentConversation(response.payload);
|
||||
setChatMode(response.payload?.chat_mode || ChatModeType.GENERAL);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load conversation:", error);
|
||||
}
|
||||
};
|
||||
const handleSendMessage = async (
|
||||
directMessage?: string,
|
||||
describe?: string,
|
||||
roadmap?: RoadMapMessage | null,
|
||||
files?: string | string[],
|
||||
) => {
|
||||
const messageContent = directMessage || input.trim();
|
||||
if (!messageContent) return;
|
||||
const currentInput = messageContent.trim();
|
||||
if (!messageContent && !filePreview.length) return;
|
||||
// Get all successfully uploaded file IDs
|
||||
let fileIds = filePreview
|
||||
.filter((file) => file.status === "success" && file.id)
|
||||
.map((file) => file.id);
|
||||
if (ScrollToBottomButtonRef.current) {
|
||||
ScrollToBottomButtonRef.current?.scrollToBottom("auto");
|
||||
}
|
||||
isAtBottom.current = true;
|
||||
let targetConversationId = state.conversationId;
|
||||
// Create user message
|
||||
const createUserMessage = (convId: string): Message => ({
|
||||
id: Date.now().toString(),
|
||||
role: MessageRole.USER,
|
||||
content: describe || messageContent,
|
||||
status: MessageState.FINISHED,
|
||||
create_time: new Date().toISOString(),
|
||||
update_time: new Date().toISOString(),
|
||||
feedback: null,
|
||||
conversation_id: convId,
|
||||
parent_message_id: "",
|
||||
type: MessageType.USER,
|
||||
files: filePreview
|
||||
.filter((file) => file.status === "success")
|
||||
.map((file) => ({
|
||||
filename: file.name,
|
||||
size: parseInt(file.size),
|
||||
url: `/api/v1/files/${file.id}/preview`,
|
||||
})),
|
||||
roadmap: roadmap || null,
|
||||
});
|
||||
setInput("");
|
||||
setFilePreview([]);
|
||||
setIsGenerating(true);
|
||||
setTaskId("");
|
||||
setShowRoadmapSelectBtn(false);
|
||||
try {
|
||||
if (!conversationId) {
|
||||
try {
|
||||
const conversationName = currentInput.slice(0, 30);
|
||||
targetConversationId = await createNewConversation(
|
||||
conversationName,
|
||||
describe || currentInput,
|
||||
chatMode,
|
||||
);
|
||||
setState({ conversationId: targetConversationId });
|
||||
} catch (error) {
|
||||
message.error("Failed to create conversation");
|
||||
console.error("Failed to create conversation:", error);
|
||||
}
|
||||
}
|
||||
// Create and add user message
|
||||
const userMessage = createUserMessage(targetConversationId);
|
||||
if (files) {
|
||||
try {
|
||||
const promptFiles = await getPromptFile(files);
|
||||
if (promptFiles?.length) {
|
||||
fileIds = promptFiles.map((item) => item.id);
|
||||
setMessages((prev) => [
|
||||
...(prev || []),
|
||||
{ ...userMessage, files: promptFiles },
|
||||
]);
|
||||
} else {
|
||||
setMessages((prev) => [...(prev || []), userMessage]);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessages((prev) => [...(prev || []), userMessage]);
|
||||
}
|
||||
} else {
|
||||
setMessages((prev) => [...(prev || []), userMessage]);
|
||||
}
|
||||
setIsThinking(true);
|
||||
setThinkingConversationId(targetConversationId);
|
||||
await sendMessageWithHandling(
|
||||
targetConversationId,
|
||||
describe || currentInput,
|
||||
userMessage.id,
|
||||
fileIds, // Pass all file IDs
|
||||
roadmap,
|
||||
);
|
||||
} catch (error) {
|
||||
setIsGenerating(false);
|
||||
resetThinkingState();
|
||||
}
|
||||
};
|
||||
const handleSelectConversation = async (conversation: Conversation) => {
|
||||
try {
|
||||
if (conversation.id === conversationId) return;
|
||||
if (isGenerating || isThinking) stopGenerating(); // Stop if message is being generated
|
||||
setCurrentConversation(conversation);
|
||||
setChatMode(conversation.chat_mode || ChatModeType.GENERAL);
|
||||
setState({ conversationId: conversation.id });
|
||||
setMessages([]);
|
||||
setShowRoadmapSelectBtn(false);
|
||||
setShowRoadmapEditBtn(false);
|
||||
await handleGetMessages(conversation.id);
|
||||
await handleGetRoadmap(conversation.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to load conversation messages and plan:", error);
|
||||
}
|
||||
};
|
||||
const setNewConversation = () => {
|
||||
if (isGenerating || isThinking) stopGenerating();
|
||||
setState({ conversationId: undefined });
|
||||
setCurrentConversation(null);
|
||||
setChatMode(ChatModeType.GENERAL);
|
||||
setMessages([]);
|
||||
setRoadmapData(null);
|
||||
setShowRoadmapSelectBtn(false);
|
||||
setShowRoadmapEditBtn(false);
|
||||
};
|
||||
|
||||
const stopGenerating = () => {
|
||||
// Use AbortController to cancel request, don't call stopChat API
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsGenerating(false);
|
||||
resetThinkingState();
|
||||
setTaskId("");
|
||||
}
|
||||
// if (isGenerating && conversationId && taskId) {
|
||||
// conversationApi.stopChat(conversationId, taskId).then((res) => {
|
||||
// if (res.status === true) {
|
||||
// setIsGenerating(false);
|
||||
// setIsThinking(false);
|
||||
// setThinkingConversationId(null);
|
||||
// setTaskId("");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
};
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
label: "Agent Workspace",
|
||||
key: "1",
|
||||
children: <Workspace />,
|
||||
},
|
||||
{
|
||||
label: "Roadmap",
|
||||
key: "roadmap",
|
||||
children: (
|
||||
<Roadmap
|
||||
data={roadmapData}
|
||||
conversationId={conversationId}
|
||||
editable={showRoadmapEditBtn}
|
||||
onSave={(data) => {
|
||||
const diffRoadmap: RoadMapMessage = {
|
||||
previous: roadmapData,
|
||||
current: data,
|
||||
};
|
||||
setShowRoadmapEditBtn(false);
|
||||
setShowRoadmapSelectBtn(false);
|
||||
handleSendMessage(roadmapUpdateQuery, "", diffRoadmap);
|
||||
setRoadmapData(data);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Sandbox",
|
||||
key: "3",
|
||||
children: <SandBox sandboxUrl={currentConversation?.sandbox_url || ""} />,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<LoginModal />
|
||||
<Sidebar
|
||||
onSelectConversation={handleSelectConversation}
|
||||
setNewConversation={setNewConversation}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
{!conversationId && (
|
||||
<div className={styles.newContainer}>
|
||||
<section className={styles.newChatSection}>
|
||||
<WelcomeView />
|
||||
<ChatInput
|
||||
setFilePreview={setFilePreview}
|
||||
handleSendMessage={handleSendMessage}
|
||||
chatMode={chatMode}
|
||||
filePreview={filePreview}
|
||||
/>
|
||||
<ChatMode chatModeValue={chatMode} setChatModeValue={setChatMode} />
|
||||
<Prompts
|
||||
handleSendMessage={handleSendMessage}
|
||||
chatMode={chatMode}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
{conversationId && (
|
||||
<div className={styles.rightContents}>
|
||||
<ChatHeader
|
||||
currentConversation={currentConversation}
|
||||
languageType={languageType}
|
||||
setLanguageType={setLanguageType}
|
||||
setCurrentConversation={setCurrentConversation}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<section className={styles.chatSection}>
|
||||
<div className={styles.chatContent}>
|
||||
<div className={styles.messageArea}>
|
||||
<ScrollToBottomButton
|
||||
className={styles.scrollWrapper}
|
||||
autoScrollThreshold={5}
|
||||
ref={ScrollToBottomButtonRef}
|
||||
>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
onClarificationSelect={handleClarificationSelect}
|
||||
isThinking={
|
||||
isThinking && thinkingConversationId === conversationId
|
||||
}
|
||||
isGenerating={isGenerating}
|
||||
ScrollToBottomButtonRef={ScrollToBottomButtonRef}
|
||||
currentConversationId={conversationId}
|
||||
startTimer={startTimer}
|
||||
/>
|
||||
{showRoadmapSelectBtn && (
|
||||
<RoadmapButton
|
||||
setShow={setShowRoadmapSelectBtn}
|
||||
handleSendMessage={handleSendMessage}
|
||||
startTimer={startTimer}
|
||||
setShowRoadmapEditBtn={setShowRoadmapEditBtn}
|
||||
/>
|
||||
)}
|
||||
</ScrollToBottomButton>
|
||||
</div>
|
||||
<ChatInput
|
||||
setFilePreview={setFilePreview}
|
||||
handleSendMessage={handleSendMessage}
|
||||
isGenerating={isGenerating}
|
||||
value={input}
|
||||
setValue={setInput}
|
||||
stopGenerating={stopGenerating}
|
||||
taskId={taskId}
|
||||
chatMode={chatMode}
|
||||
filePreview={filePreview}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.workspaceSection}>
|
||||
<Tabs
|
||||
className={styles.tabSpace}
|
||||
defaultActiveKey="1"
|
||||
items={items}
|
||||
activeKey={activeKey}
|
||||
onChange={setActiveKey}
|
||||
type="segmented"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
189
alias/frontend/src/pages/Chat/utils.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { fileApi } from "@/services/api/file";
|
||||
import { ApiMessage } from "@/types/api";
|
||||
import {
|
||||
FeedbackType,
|
||||
Message,
|
||||
MessageRole,
|
||||
MessageState,
|
||||
MessageType,
|
||||
SelectionType,
|
||||
ToolIconType,
|
||||
} from "@/types/message";
|
||||
import { MAX_FILE_SIZE } from "@/utils/constant";
|
||||
import { message } from "@agentscope-ai/design";
|
||||
const sampleFiles = import.meta.glob("@/assets/file/*");
|
||||
// Map API message to chat message
|
||||
const mapApiMessageToChatMessage = (
|
||||
msg: ApiMessage,
|
||||
generating: boolean = false,
|
||||
): Message => {
|
||||
const baseMessage = {
|
||||
...msg?.message,
|
||||
id: msg.id,
|
||||
role:
|
||||
msg.message.role === "user" ? MessageRole.USER : MessageRole.ASSISTANT,
|
||||
content: msg.message.content,
|
||||
status: msg.message.status as MessageState,
|
||||
create_time: msg.create_time,
|
||||
update_time: msg.update_time,
|
||||
feedback: msg.feedback
|
||||
? msg.feedback === "like"
|
||||
? FeedbackType.LIKE
|
||||
: FeedbackType.DISLIKE
|
||||
: null,
|
||||
conversation_id: msg.conversation_id,
|
||||
parent_message_id: msg.parent_message_id || "",
|
||||
isGenerating: generating,
|
||||
meta_data: msg.meta_data,
|
||||
task_id: msg.task_id,
|
||||
};
|
||||
|
||||
// Return directly if it's a user message
|
||||
if (msg.message.role === "user") {
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.USER,
|
||||
};
|
||||
}
|
||||
|
||||
// Determine agent message type
|
||||
switch (msg.message.type) {
|
||||
case "response":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.RESPONSE,
|
||||
};
|
||||
case "thought":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.THOUGHT,
|
||||
};
|
||||
case "sub_response":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.SUB_RESPONSE,
|
||||
};
|
||||
case "sub_thought":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.SUB_THOUGHT,
|
||||
};
|
||||
case "tool_call":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.TOOL_CALL,
|
||||
name: msg.message.name || "",
|
||||
arguments: msg.message.arguments || {},
|
||||
icon: (msg.message.icon as ToolIconType) || ToolIconType.TOOL,
|
||||
content: msg.message.content,
|
||||
tool_name: msg.message.tool_name || "",
|
||||
};
|
||||
case "tool_use":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.TOOL_USE,
|
||||
name: msg.message.name || "",
|
||||
arguments: msg.message.arguments || {},
|
||||
icon: (msg.message.icon as ToolIconType) || ToolIconType.TOOL,
|
||||
content: msg.message.content,
|
||||
tool_name: msg.message.tool_name || "",
|
||||
};
|
||||
case "tool_result":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.TOOL_RESULT,
|
||||
name: msg.message.name || "",
|
||||
arguments: msg.message.arguments || {},
|
||||
icon: (msg.message.icon as ToolIconType) || ToolIconType.TOOL,
|
||||
content: msg.message.content,
|
||||
tool_name: msg.message.tool_name || "",
|
||||
};
|
||||
case "clarification":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.CLARIFICATION,
|
||||
options: msg.message.options || [],
|
||||
selection_type:
|
||||
msg.message.selection_type === "multiple"
|
||||
? SelectionType.MULTIPLE
|
||||
: SelectionType.SINGLE,
|
||||
};
|
||||
case "files":
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.FILES,
|
||||
files: msg.message.files || [],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...baseMessage,
|
||||
type: MessageType.RESPONSE,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getPromptFile = async (files: string | string[]) => {
|
||||
try {
|
||||
const filePaths = Object.keys(sampleFiles);
|
||||
if (filePaths.length === 0) {
|
||||
console.warn("No files found in assets/file directory");
|
||||
return null;
|
||||
}
|
||||
const paths = Array.isArray(files) ? files : [files];
|
||||
const selectedFilePaths = filePaths.filter((filePath) =>
|
||||
paths.includes(filePath),
|
||||
);
|
||||
|
||||
if (selectedFilePaths.length === 0) {
|
||||
console.warn("No matching files found for paths:", paths);
|
||||
return null;
|
||||
}
|
||||
const fileProcessingPromises = selectedFilePaths.map(
|
||||
async (selectedFilePath) => {
|
||||
try {
|
||||
const fileName = selectedFilePath.split("/").pop() || "sample.csv";
|
||||
const response = await fetch(selectedFilePath);
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`Failed to fetch file: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], fileName, { type: blob.type });
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
message.error(`File ${file.name} exceeds 10MB limit`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadResponse: any = await fileApi.uploadFile(file);
|
||||
if (uploadResponse?.status && uploadResponse?.payload) {
|
||||
const { filename, id, size } = uploadResponse.payload;
|
||||
return {
|
||||
filename,
|
||||
size,
|
||||
url: `/api/v1/files/${id}/preview`,
|
||||
id,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (fileError) {
|
||||
console.warn(
|
||||
`Failed to process file ${selectedFilePath}:`,
|
||||
fileError,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
const results = await Promise.all(fileProcessingPromises);
|
||||
const fileResults = results.filter((result) => result !== null);
|
||||
return fileResults.length > 0 ? fileResults : null;
|
||||
} catch (error) {
|
||||
console.error("Failed to load or upload sample files:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { getPromptFile, mapApiMessageToChatMessage };
|
||||
75
alias/frontend/src/pages/Login/index.module.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: var(--sps-color-bg-base);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.logWrap {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #e3e3e3;
|
||||
border-radius: 1rem;
|
||||
// height: 544px;
|
||||
// width: 448px;
|
||||
// background-color: rgba(0, 0, 0, 0) 0px 0px 0px 0px;
|
||||
// box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
.logBtn{
|
||||
opacity: 1;
|
||||
background-color: rgb(97 92 237);
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
cursor: pointer;
|
||||
width: 336px;
|
||||
margin-top: 30px;
|
||||
color: rgb(255 255 255);
|
||||
font-size: 100%;
|
||||
&:hover {
|
||||
background-color: rgb(97, 92, 237, 0.9);
|
||||
}
|
||||
}
|
||||
.disablebBtn{
|
||||
opacity: 1;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.5rem;
|
||||
width: 336px;
|
||||
margin-top: 30px;
|
||||
font-size: 100%;
|
||||
color: #999;
|
||||
border-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:global {
|
||||
.sps-pro-form-login-container {
|
||||
height: 100%;
|
||||
background-color: none;
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #e3e3e3;
|
||||
border-radius: 1rem;
|
||||
width: 448px;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.sps-pro-form-login-main {
|
||||
margin-top: 40px;
|
||||
.ant-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
alias/frontend/src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { loginApi } from "@/services/api/login";
|
||||
import { message } from "@agentscope-ai/design";
|
||||
import {
|
||||
SparkEmailLine,
|
||||
SparkLockLine,
|
||||
SparkUserLine,
|
||||
} from "@agentscope-ai/icons";
|
||||
import type { ProFormInstance } from "@ant-design/pro-components";
|
||||
import {
|
||||
LoginForm,
|
||||
ProConfigProvider,
|
||||
ProFormText,
|
||||
} from "@ant-design/pro-components";
|
||||
import { theme } from "antd";
|
||||
import { useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
export const Login = () => {
|
||||
const { token } = theme.useToken();
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get("mode");
|
||||
const navigate = useNavigate();
|
||||
const onFinish = async () => {
|
||||
try {
|
||||
const values = await formRef?.current?.validateFields();
|
||||
if (mode === "register") {
|
||||
delete values?.repassword;
|
||||
const register = await loginApi.register(values);
|
||||
message.success("Registration successful");
|
||||
navigate("/login?mode=login");
|
||||
const { payload } = register;
|
||||
if (payload?.access_token)
|
||||
localStorage.setItem("access_token", payload?.access_token);
|
||||
if (payload?.refresh_token)
|
||||
localStorage.setItem("refresh_token", payload?.refresh_token);
|
||||
// console.log(register, "register");
|
||||
}
|
||||
if (mode === "login") {
|
||||
const login = await loginApi.login(values);
|
||||
const { payload } = login;
|
||||
if (payload?.access_token)
|
||||
localStorage.setItem("access_token", payload?.access_token);
|
||||
if (payload?.refresh_token)
|
||||
localStorage.setItem("refresh_token", payload?.refresh_token);
|
||||
navigate("/");
|
||||
}
|
||||
} catch (errorInfo: any) {
|
||||
if (mode === "login") {
|
||||
message.error(errorInfo?.response?.data?.detail || "Login failed");
|
||||
}
|
||||
if (mode === "register") {
|
||||
message.error(
|
||||
errorInfo?.response?.data?.detail || "Registration failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logWrap}>
|
||||
<ProConfigProvider hashed={false}>
|
||||
<div style={{ backgroundColor: token.colorBgContainer }}>
|
||||
<LoginForm title="AgentScope" formRef={formRef} onFinish={onFinish}>
|
||||
{mode === "register" && (
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <SparkUserLine className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"Please enter your username"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter your username!",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProFormText
|
||||
name="email"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <SparkEmailLine className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"Please enter your email"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter your email!",
|
||||
},
|
||||
{
|
||||
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: "Please enter a valid email address",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <SparkLockLine className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"Please enter your password"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter your password",
|
||||
},
|
||||
{
|
||||
pattern: /^\S{2,40}$/,
|
||||
message: "Please enter a valid password",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{mode === "register" && (
|
||||
<ProFormText.Password
|
||||
name="repassword"
|
||||
fieldProps={{
|
||||
size: "large",
|
||||
prefix: <SparkLockLine className={"prefixIcon"} />,
|
||||
}}
|
||||
placeholder={"Please enter your password again"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please enter your password",
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("password") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error("Passwords do not match, please re-enter"),
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</LoginForm>
|
||||
</div>
|
||||
</ProConfigProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Login;
|
||||
319
alias/frontend/src/pages/SharePage/index.module.scss
Normal file
@@ -0,0 +1,319 @@
|
||||
.sharePage {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--message-border);
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 72px;
|
||||
.conversationName {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
font-size: 16px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--artifacts-wrap-bg);
|
||||
box-shadow: 0 2px 4px var(--sidebar-conversation-hover-bg);
|
||||
color: var(--text-secondary);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.messageList {
|
||||
flex: 4;
|
||||
overflow-y: auto;
|
||||
background: var(--message-content);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rightTabs {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: var(--message-content);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--share-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabHeader {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--share-border);
|
||||
padding: 24px 24px 0 24px;
|
||||
gap: 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: var(--message-content);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: #222;
|
||||
cursor: pointer;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
.tabActive::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #1677ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0 24px 24px 24px;
|
||||
background: var(--message-content);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--message-content);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
:global {
|
||||
.workWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--message-content);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.workspaceHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 19px 25px 19px 35px;
|
||||
border-bottom: 1px solid var(--share-border);
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
line-height: 28px;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.collectButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #000;
|
||||
background: var(--message-content);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--artifacts-wrap-bg);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspaceContent {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.roadmap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--message-content);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.messageList {
|
||||
overflow-y: auto;
|
||||
background: var(--artifacts-wrap-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
|
||||
:global {
|
||||
.message {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--message-content);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.roadmap {
|
||||
overflow-y: auto;
|
||||
background: var(--message-content);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-size: 16px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--message-content);
|
||||
border-top: 1px solid var(--share-border);
|
||||
padding: 16px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.progressContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controlButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--share-border);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--message-border);
|
||||
}
|
||||
}
|
||||
|
||||
.progressDot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--message-content);
|
||||
border: 2px solid #1890ff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||