mirror of
https://github.com/mosh-hamedani/helpdesk.git
synced 2026-05-21 11:58:19 +02:00
Add the ability to summarize tickets
This commit is contained in:
parent
caddd8e0a0
commit
199a58a3a4
|
|
@ -9,7 +9,7 @@ A ticket management system that uses AI to classify, respond to, and route suppo
|
|||
- **Frontend**: React + TypeScript + Vite (port 5173) + shadcn/ui
|
||||
- **Backend**: Express + TypeScript + Bun (port 3000)
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **AI**: Claude API (Anthropic)
|
||||
- **AI**: OpenAI GPT-5 Nano via Vercel AI SDK (`@ai-sdk/openai`)
|
||||
- **Auth**: Better Auth (email/password, database sessions)
|
||||
|
||||
## Project Structure
|
||||
|
|
|
|||
|
|
@ -71,13 +71,12 @@ export default function ReplyThread({ ticket }: ReplyThreadProps) {
|
|||
<CardContent>
|
||||
{reply.bodyHtml ? (
|
||||
<div
|
||||
className="text-sm"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(reply.bodyHtml),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p className="whitespace-pre-wrap text-sm">{reply.body}</p>
|
||||
<p>{reply.body}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
104
client/src/components/TicketSummary.test.tsx
Normal file
104
client/src/components/TicketSummary.test.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import axios from "axios";
|
||||
import { renderWithQuery } from "@/test/render";
|
||||
import TicketSummary from "./TicketSummary";
|
||||
|
||||
vi.mock("axios");
|
||||
const mockedAxios = vi.mocked(axios, { deep: true });
|
||||
|
||||
const TICKET_ID = 42;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
function renderSummary() {
|
||||
const user = userEvent.setup();
|
||||
renderWithQuery(<TicketSummary ticket={{ id: TICKET_ID }} />);
|
||||
return { user };
|
||||
}
|
||||
|
||||
describe("TicketSummary", () => {
|
||||
it("should render Summarize button", () => {
|
||||
renderSummary();
|
||||
|
||||
expect(screen.getByRole("button", { name: "Summarize" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show summary card before clicking", () => {
|
||||
renderSummary();
|
||||
|
||||
expect(screen.queryByText("Summary")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call POST /api/tickets/:ticketId/replies/summarize on click", async () => {
|
||||
mockedAxios.post.mockResolvedValue({ data: { summary: "A summary" } });
|
||||
const { user } = renderSummary();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/api/tickets/${TICKET_ID}/replies/summarize`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should display summary text on success", async () => {
|
||||
mockedAxios.post.mockResolvedValue({
|
||||
data: { summary: "Customer asked about refund. Agent provided instructions." },
|
||||
});
|
||||
const { user } = renderSummary();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Customer asked about refund. Agent provided instructions.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show 'Summarizing...' while loading", async () => {
|
||||
mockedAxios.post.mockReturnValue(new Promise(() => {}));
|
||||
const { user } = renderSummary();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
|
||||
await waitFor(() => {
|
||||
const button = screen.getByRole("button", { name: "Summarizing..." });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error on failure", async () => {
|
||||
mockedAxios.post.mockRejectedValue(new Error("Service unavailable"));
|
||||
mockedAxios.isAxiosError.mockReturnValue(false);
|
||||
const { user } = renderSummary();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Failed to generate summary")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should re-generate summary on subsequent clicks", async () => {
|
||||
mockedAxios.post
|
||||
.mockResolvedValueOnce({ data: { summary: "First summary" } })
|
||||
.mockResolvedValueOnce({ data: { summary: "Updated summary" } });
|
||||
const { user } = renderSummary();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("First summary")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Summarize" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Updated summary")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
55
client/src/components/TicketSummary.tsx
Normal file
55
client/src/components/TicketSummary.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { type Ticket } from "core/constants/ticket.ts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import ErrorAlert from "@/components/ErrorAlert";
|
||||
|
||||
interface TicketSummaryProps {
|
||||
ticket: Ticket;
|
||||
}
|
||||
|
||||
export default function TicketSummary({ ticket }: TicketSummaryProps) {
|
||||
const summarizeMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await axios.post(
|
||||
`/api/tickets/${ticket.id}/replies/summarize`
|
||||
);
|
||||
return data.summary as string;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => summarizeMutation.mutate()}
|
||||
disabled={summarizeMutation.isPending}
|
||||
>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
{summarizeMutation.isPending ? "Summarizing..." : "Summarize"}
|
||||
</Button>
|
||||
|
||||
{summarizeMutation.error && (
|
||||
<ErrorAlert
|
||||
error={summarizeMutation.error}
|
||||
fallback="Failed to generate summary"
|
||||
/>
|
||||
)}
|
||||
|
||||
{summarizeMutation.data && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium">Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-wrap">
|
||||
{summarizeMutation.data}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import TicketDetail from "@/components/TicketDetail";
|
|||
import UpdateTicket from "@/components/UpdateTicket";
|
||||
import ReplyThread from "@/components/ReplyThread";
|
||||
import ReplyForm from "@/components/ReplyForm";
|
||||
import TicketSummary from "@/components/TicketSummary";
|
||||
|
||||
export default function TicketDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
|
@ -42,6 +43,8 @@ export default function TicketDetailPage() {
|
|||
<div className="space-y-6">
|
||||
<TicketDetail ticket={ticket} />
|
||||
|
||||
<TicketSummary ticket={ticket} />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2>Replies</h2>
|
||||
<ReplyThread ticket={ticket} />
|
||||
|
|
|
|||
397
server/prisma/seed-replies.ts
Normal file
397
server/prisma/seed-replies.ts
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
import "dotenv/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "../src/generated/prisma/client";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const TICKET_ID = 110;
|
||||
|
||||
const customerReplies = [
|
||||
`Hi there, I'm reaching out because I've been having a really frustrating experience
|
||||
with the course platform over the past few days. Every time I try to access
|
||||
the "Advanced React Patterns" module, the video player just shows a black screen.
|
||||
I've tried refreshing the page multiple times, clearing my browser cache,
|
||||
and even switching to a different browser, but nothing seems to work.
|
||||
This is really disappointing because I was making great progress through
|
||||
the course and this issue has completely halted my learning.
|
||||
I'm currently using Chrome on Windows 11, and my internet connection
|
||||
is stable — I can stream videos on other platforms without any issues.
|
||||
Could you please look into this as soon as possible?
|
||||
I'd really appreciate any help you can provide.`,
|
||||
|
||||
`Thank you for the quick response! I really appreciate you looking into this.
|
||||
I went ahead and tried the steps you suggested. I disabled all my browser
|
||||
extensions, cleared the cache completely, and restarted Chrome.
|
||||
Unfortunately, the issue is still persisting. The video player loads
|
||||
the controls and the progress bar, but the actual video content
|
||||
remains completely black. I also noticed that the video thumbnail
|
||||
shows correctly on the course overview page, so it seems like
|
||||
the video files themselves might be fine.
|
||||
One thing I forgot to mention earlier — this issue started happening
|
||||
right after the platform update that rolled out last Tuesday.
|
||||
Before that update, everything was working perfectly fine.
|
||||
Could this be related to some change in the video player component?`,
|
||||
|
||||
`I just tried Firefox as you suggested, and interestingly the videos
|
||||
do play there, but the quality is noticeably lower and there's
|
||||
a significant buffering delay that makes it hard to follow along.
|
||||
The playback keeps pausing every 10-15 seconds to buffer more content,
|
||||
even though my connection speed is well above the recommended minimum.
|
||||
I ran a speed test and I'm getting 150 Mbps down and 50 Mbps up,
|
||||
so bandwidth definitely shouldn't be an issue here.
|
||||
Also, I noticed that in Firefox, the video player UI looks different
|
||||
from what I remember — the controls are positioned differently
|
||||
and the fullscreen mode doesn't seem to work properly.
|
||||
Is there a known compatibility issue with the latest Firefox version?
|
||||
I'm on Firefox 128.0.3 if that helps with troubleshooting.`,
|
||||
|
||||
`Sure, I'd be happy to provide more details about my setup.
|
||||
My operating system is Windows 11 Pro, version 23H2, build 22631.
|
||||
My display resolution is 2560x1440 at 144Hz refresh rate.
|
||||
I have a dedicated NVIDIA RTX 3070 graphics card with the latest
|
||||
driver version 551.86 installed. Hardware acceleration is enabled
|
||||
in Chrome, which I know can sometimes cause issues with video playback.
|
||||
I also have an ad blocker (uBlock Origin) and a password manager
|
||||
extension installed, but I did try disabling both of them as you
|
||||
suggested earlier and the problem persisted.
|
||||
My Chrome version is 124.0.6367.91 — I just checked and it says
|
||||
it's up to date. Let me know if you need any other information
|
||||
from my end. I'm really hoping we can get this resolved soon
|
||||
because I have a project deadline coming up next week.`,
|
||||
|
||||
`That's really helpful to know — thank you for explaining the root cause!
|
||||
I went ahead and disabled hardware acceleration in Chrome as you suggested,
|
||||
and I can confirm that the videos are now playing correctly.
|
||||
The black screen issue is completely gone, which is a huge relief.
|
||||
However, I did notice that with hardware acceleration disabled,
|
||||
the overall browser performance feels a bit sluggish, especially
|
||||
when I have multiple tabs open. Scrolling through the course material
|
||||
isn't as smooth as it used to be, and some of the interactive
|
||||
code examples in the exercises seem to render more slowly.
|
||||
Is this expected? And more importantly, is the team planning
|
||||
to release a fix so that hardware acceleration can work alongside
|
||||
the video player without causing the black screen issue?
|
||||
I'd prefer not to keep hardware acceleration disabled permanently.`,
|
||||
|
||||
`That's great news! I'm glad to hear the team is already working on a fix.
|
||||
The timeline of one to two weeks sounds reasonable. In the meantime,
|
||||
I'll keep hardware acceleration disabled as a workaround.
|
||||
I do have a follow-up question though — while I was troubleshooting
|
||||
this issue, I noticed that some of the downloadable resources
|
||||
for the React course are also not working. Specifically, the
|
||||
exercise files for chapters 7 through 12 give me a 404 error
|
||||
when I click the download link. I'm not sure if this is related
|
||||
to the same platform update or if it's a separate issue entirely.
|
||||
Should I create a new ticket for this, or can you look into it
|
||||
as part of this conversation? I've attached a screenshot of the
|
||||
error page in case that helps. It shows the exact URL that's
|
||||
returning the 404 and the timestamp of when I tried accessing it.`,
|
||||
|
||||
`Perfect, I appreciate you handling both issues in one ticket.
|
||||
I just checked the download links again and the ones for chapters
|
||||
1 through 6 are working fine — I was able to download all of those
|
||||
without any problems. It's specifically chapters 7 through 12
|
||||
that are broken. The URLs for the working chapters follow the pattern
|
||||
/downloads/react-course/ch01-exercises.zip, but when I look at the
|
||||
broken links, they seem to have a slightly different URL structure:
|
||||
/downloads/react-course/v2/ch07-exercises.zip. Notice the extra
|
||||
"v2" in the path. I'm wondering if the resources were moved to
|
||||
a new location during the platform update but the links in the
|
||||
course page weren't updated to reflect the new paths.
|
||||
I hope this information helps narrow down the issue!
|
||||
Let me know if you need me to test anything else.`,
|
||||
|
||||
`Awesome, the download links are all working now! I just went through
|
||||
and downloaded every exercise file from chapters 7 through 12
|
||||
and they all came through without any issues. Thank you so much
|
||||
for the quick turnaround on that fix.
|
||||
Now I'm back on track with the course, but I wanted to mention
|
||||
one more thing. While I was catching up on the modules I missed,
|
||||
I noticed that the code editor in the interactive exercises has
|
||||
a new feature where it auto-saves your work. This is really nice,
|
||||
but I found that it seems to overwrite the original starter code
|
||||
even when you reset the exercise. So if I make changes, then click
|
||||
"Reset Exercise," it loads my auto-saved version instead of the
|
||||
original template code. This kind of defeats the purpose of the
|
||||
reset button. Is this a known issue or am I misunderstanding
|
||||
how the reset feature is supposed to work?`,
|
||||
|
||||
`I just tested it again following your instructions exactly.
|
||||
Here's what I did step by step: I opened the exercise for chapter 9,
|
||||
lesson 3. I modified the starter code by adding a useEffect hook.
|
||||
I waited about 30 seconds to make sure the auto-save triggered.
|
||||
Then I clicked the "Reset Exercise" button at the top right corner.
|
||||
The editor briefly flashed, but then it showed my modified code
|
||||
with the useEffect hook still there, not the original starter code.
|
||||
I tried this three more times with the same result each time.
|
||||
Then I opened a completely different exercise (chapter 10, lesson 1)
|
||||
and that one reset correctly because I hadn't modified it before.
|
||||
So it definitely seems like the auto-save is interfering with
|
||||
the reset functionality. I cleared my browser's local storage
|
||||
for the site, and after that the reset worked correctly again,
|
||||
but the auto-save data for all my other exercises was lost too.`,
|
||||
|
||||
`That makes total sense — storing the auto-save and the starter code
|
||||
separately would definitely solve the problem. I appreciate you
|
||||
explaining the technical details of how it works behind the scenes.
|
||||
While we're on the topic of the interactive exercises, I have
|
||||
one more piece of feedback. The console output panel at the bottom
|
||||
of the code editor is really useful for debugging, but the font
|
||||
size is quite small and there doesn't seem to be a way to resize it.
|
||||
For someone who spends hours working through exercises, it would be
|
||||
really nice to either have a larger default font size or the ability
|
||||
to drag the panel border to make it bigger. I know this is more
|
||||
of a feature request than a bug, but I thought I'd mention it
|
||||
while we're already discussing the exercise environment.
|
||||
Also, is there a way to change the editor theme to dark mode?
|
||||
I've been looking for a setting but couldn't find one.`,
|
||||
];
|
||||
|
||||
const agentReplies = [
|
||||
`Hi! Thank you so much for reaching out to us about this issue.
|
||||
I'm sorry to hear you're experiencing trouble with the video player —
|
||||
I completely understand how frustrating that must be, especially
|
||||
when you're in the middle of making progress through the course.
|
||||
Let me help you troubleshoot this step by step.
|
||||
First, could you try disabling all browser extensions temporarily?
|
||||
Sometimes extensions like ad blockers or privacy tools can interfere
|
||||
with our video player's functionality. To do this, go to Chrome's
|
||||
menu, click "Extensions," and toggle off all of them.
|
||||
After that, please try loading the video again.
|
||||
If that doesn't work, try opening the course in an incognito window
|
||||
(Ctrl+Shift+N in Chrome) to see if the issue persists there.
|
||||
This will help us determine if the problem is related to cached
|
||||
data or extensions. Please let me know what you find!`,
|
||||
|
||||
`Thank you for trying those steps and for the additional detail
|
||||
about the timing — that's actually very helpful information!
|
||||
You're right to suspect that the recent platform update could
|
||||
be related. We did roll out some changes to our video streaming
|
||||
infrastructure last Tuesday, and your report matches a pattern
|
||||
we've been seeing from a small number of users.
|
||||
The issue appears to be related to how our updated video player
|
||||
interacts with certain browser configurations.
|
||||
Could you try one more thing for me? Please open the affected
|
||||
video page, then press F12 to open Chrome's Developer Tools.
|
||||
Click on the "Console" tab and let me know if you see any
|
||||
error messages highlighted in red. Specifically, I'm looking
|
||||
for errors that mention "MediaSource" or "codec."
|
||||
Also, could you try accessing the videos in Firefox to see
|
||||
if the issue is Chrome-specific? This will help us narrow down
|
||||
the root cause significantly.`,
|
||||
|
||||
`That's very interesting — the fact that it works in Firefox
|
||||
(even with performance issues) but not in Chrome strongly suggests
|
||||
this is related to a Chrome-specific video codec handling change.
|
||||
The buffering issues you're experiencing in Firefox are likely
|
||||
because our player is falling back to a less optimized streaming
|
||||
protocol when it can't use the preferred one.
|
||||
I've escalated this to our engineering team with all the details
|
||||
you've provided. They're already investigating the Chrome compatibility
|
||||
issue that came with the last update.
|
||||
In the meantime, could you provide me with a few more details
|
||||
about your system? Specifically, your operating system version,
|
||||
display resolution, whether you have a dedicated graphics card,
|
||||
and whether hardware acceleration is enabled in Chrome.
|
||||
You can check the hardware acceleration setting by going to
|
||||
chrome://settings, searching for "hardware acceleration,"
|
||||
and letting me know if it's turned on or off.
|
||||
These details will help our engineers reproduce the issue.`,
|
||||
|
||||
`Thank you for providing such thorough details about your setup!
|
||||
I shared all of this information with our engineering team, and
|
||||
they've been able to reproduce the issue on a similar configuration.
|
||||
The root cause has been identified — it's a conflict between our
|
||||
updated video player's hardware-accelerated rendering pipeline
|
||||
and certain NVIDIA driver versions when running at high refresh
|
||||
rates (above 60Hz). The combination of your 144Hz display and
|
||||
the RTX 3070 with the latest drivers triggers a rendering bug
|
||||
in Chrome's compositor that results in the black screen you're seeing.
|
||||
As an immediate workaround, could you try disabling hardware
|
||||
acceleration in Chrome? Go to chrome://settings, search for
|
||||
"Use hardware acceleration when available," and toggle it off.
|
||||
Then restart Chrome completely and try playing the video again.
|
||||
Our team is working on a patch for the video player that will
|
||||
resolve this conflict. I expect the fix to be deployed within
|
||||
the next one to two weeks. I'll make sure to update you
|
||||
when the fix is live so you can re-enable hardware acceleration.`,
|
||||
|
||||
`I'm really glad to hear that the workaround resolved the black
|
||||
screen issue! And yes, you're absolutely right that disabling
|
||||
hardware acceleration can affect overall browser performance —
|
||||
that's a known trade-off, especially on systems with powerful
|
||||
dedicated GPUs like yours where Chrome offloads a lot of
|
||||
rendering work to the graphics card.
|
||||
To answer your questions: yes, the slight sluggishness with
|
||||
multiple tabs and the slower rendering of interactive elements
|
||||
is expected when hardware acceleration is disabled. Chrome has
|
||||
to fall back to software rendering for everything, which puts
|
||||
more load on your CPU instead of leveraging the GPU.
|
||||
Regarding the fix timeline — our engineering team has already
|
||||
identified the exact issue in the video player code and they're
|
||||
currently testing a patch. The estimated deployment is within
|
||||
one to two weeks. Once it's live, you'll be able to re-enable
|
||||
hardware acceleration and everything should work perfectly.
|
||||
I'll personally follow up with you once the fix is deployed.
|
||||
In the meantime, please don't hesitate to reach out if you
|
||||
experience any other issues!`,
|
||||
|
||||
`Of course! I'd be happy to look into the download issue as well —
|
||||
no need to create a separate ticket. Let me check on those
|
||||
exercise files for chapters 7 through 12 right away.
|
||||
I've just checked our content management system and I can see
|
||||
the issue. You're absolutely right that the files were moved
|
||||
during the platform update. The exercise files for the newer
|
||||
chapters were migrated to a new storage location as part of
|
||||
our infrastructure upgrade, but it looks like the download links
|
||||
in the course page weren't properly updated to point to the
|
||||
new location. This is definitely a bug on our end.
|
||||
I'm going to flag this for our content team right now so they
|
||||
can update the links. In the meantime, could you confirm exactly
|
||||
which chapter exercise files are affected? You mentioned chapters
|
||||
7 through 12, but I want to make sure I report the complete
|
||||
list. Also, are the video resources and supplementary PDFs
|
||||
downloading correctly, or are those affected too?
|
||||
Thank you for your patience while we sort this out!`,
|
||||
|
||||
`Great detective work on the URL structure difference! You're
|
||||
absolutely spot on — the "v2" path segment is from our new
|
||||
content delivery system, and the course page links weren't
|
||||
updated after the migration. I've reported this to our content
|
||||
team with the specific URL pattern details you provided.
|
||||
I'm happy to report that the fix has already been deployed!
|
||||
Our content team was able to push an update to correct all the
|
||||
download links for the React course within the last hour.
|
||||
Could you please try downloading the exercise files for chapters
|
||||
7 through 12 again and let me know if they're all working now?
|
||||
If any of them are still broken, please let me know the specific
|
||||
chapter and lesson number and I'll escalate it immediately.
|
||||
Also, I wanted to give you a quick update on the video player
|
||||
fix — our engineering team completed their testing yesterday
|
||||
and the patch is scheduled for deployment early next week.
|
||||
Once it's out, you should be able to re-enable hardware
|
||||
acceleration in Chrome without any issues.
|
||||
Thank you for your patience throughout all of this!`,
|
||||
|
||||
`Wonderful — I'm glad all the download links are working now!
|
||||
Regarding the auto-save and reset issue you've discovered,
|
||||
that's a really good catch. Let me explain what's happening.
|
||||
The auto-save feature was introduced in the latest update to
|
||||
prevent users from losing their work if they accidentally close
|
||||
the browser or navigate away from the page. It saves your code
|
||||
to the browser's local storage every 30 seconds.
|
||||
The problem is that the reset function is currently looking at
|
||||
local storage first before falling back to the original template.
|
||||
So when you click "Reset Exercise," it finds your auto-saved
|
||||
version and loads that instead of the original starter code.
|
||||
This is definitely a bug — the reset button should always restore
|
||||
the original template code regardless of any auto-saved versions.
|
||||
Could you try one more test for me? When you click "Reset Exercise,"
|
||||
do you see a confirmation dialog asking you to confirm the reset,
|
||||
or does it just immediately reload? This will help me understand
|
||||
which version of the reset logic your session is using.`,
|
||||
|
||||
`Thank you for the detailed reproduction steps — that's exactly
|
||||
the kind of information our developers need to fix this efficiently!
|
||||
I've filed a bug report with your step-by-step reproduction case
|
||||
and the observation that clearing local storage resolves it
|
||||
(but with the side effect of losing all auto-save data).
|
||||
The fix for this will involve storing the auto-saved code and
|
||||
the original starter code in separate local storage keys.
|
||||
That way, the reset function can always retrieve the original
|
||||
template without being affected by the auto-save feature.
|
||||
Our development team has added this to the current sprint,
|
||||
so you should see a fix within the next week or two.
|
||||
In the meantime, if you need to reset an exercise, you can
|
||||
use this workaround: open Chrome DevTools (F12), go to the
|
||||
Application tab, expand Local Storage on the left, find the
|
||||
entry for our domain, and look for the key that starts with
|
||||
"exercise-autosave-" followed by the exercise ID. Delete just
|
||||
that one entry and then refresh the page. This will reset
|
||||
that specific exercise without affecting your other saved work.
|
||||
I know it's not ideal, but it's better than clearing everything!`,
|
||||
|
||||
`Those are both excellent suggestions, and I really appreciate
|
||||
you taking the time to share that feedback with us!
|
||||
Regarding the console output font size — you're absolutely right
|
||||
that it could be improved. I've logged this as a feature request
|
||||
with our UX team. In the meantime, there's actually a keyboard
|
||||
shortcut you can use: hold Ctrl (or Cmd on Mac) and press the
|
||||
plus key (+) to zoom in on just the console panel when it's focused.
|
||||
It's not a perfect solution, but it should help with readability.
|
||||
As for dark mode in the code editor — great news! This feature
|
||||
is actually already available but a bit hidden. Click on the gear
|
||||
icon in the top-right corner of the code editor panel (not the
|
||||
main site settings). You'll see a dropdown called "Editor Theme"
|
||||
with options including "VS Code Dark," "Monokai," and "Dracula,"
|
||||
among others. Let me know if you can find it!
|
||||
I want to thank you for being such a thorough and helpful user.
|
||||
Your detailed bug reports have helped us identify and fix several
|
||||
issues that will benefit all our students. If you run into
|
||||
anything else, please don't hesitate to reach out!`,
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const ticket = await prisma.ticket.findUnique({
|
||||
where: { id: TICKET_ID },
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
console.error(`Ticket ${TICKET_ID} not found. Please ensure it exists.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find an agent user to attribute agent replies to
|
||||
const agent = await prisma.user.findFirst({
|
||||
where: { role: "agent" },
|
||||
});
|
||||
// Fall back to any user if no agent exists
|
||||
const agentUser =
|
||||
agent ?? (await prisma.user.findFirst({ where: { role: "admin" } }));
|
||||
|
||||
if (!agentUser) {
|
||||
console.error("No user found to attribute agent replies to.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Delete existing replies for this ticket
|
||||
const deleted = await prisma.reply.deleteMany({
|
||||
where: { ticketId: TICKET_ID },
|
||||
});
|
||||
console.log(`Deleted ${deleted.count} existing replies for ticket ${TICKET_ID}.`);
|
||||
|
||||
// Create 20 replies alternating between agent and customer
|
||||
const baseDate = new Date("2026-02-20T09:00:00Z");
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const isAgent = i % 2 === 1; // Even = customer, Odd = agent
|
||||
const replyIndex = Math.floor(i / 2);
|
||||
const body = isAgent ? agentReplies[replyIndex] : customerReplies[replyIndex];
|
||||
|
||||
// Space replies ~2 hours apart
|
||||
const createdAt = new Date(baseDate.getTime() + i * 2 * 60 * 60 * 1000);
|
||||
|
||||
await prisma.reply.create({
|
||||
data: {
|
||||
body,
|
||||
senderType: isAgent ? "agent" : "customer",
|
||||
ticketId: TICKET_ID,
|
||||
userId: isAgent ? agentUser.id : null,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
|
||||
const sender = isAgent ? `Agent (${agentUser.name})` : "Customer";
|
||||
console.log(`Created reply ${i + 1}/20 — ${sender} at ${createdAt.toISOString()}`);
|
||||
}
|
||||
|
||||
console.log(`\nDone! Seeded 20 replies for ticket ${TICKET_ID}.`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
|
@ -60,6 +60,48 @@ router.post("/", requireAuth, async (req, res) => {
|
|||
res.status(201).json(reply);
|
||||
});
|
||||
|
||||
router.post("/summarize", requireAuth, async (req, res) => {
|
||||
const ticketId = parseId(req.params.ticketId);
|
||||
if (!ticketId) {
|
||||
res.status(400).json({ error: "Invalid ticket ID" });
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await prisma.ticket.findUnique({ where: { id: ticketId } });
|
||||
if (!ticket) {
|
||||
res.status(404).json({ error: "Ticket not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const replies = await prisma.reply.findMany({
|
||||
where: { ticketId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: { user: { select: { name: true } } },
|
||||
});
|
||||
|
||||
const conversation = replies
|
||||
.map((r) => {
|
||||
const sender =
|
||||
r.senderType === "agent" ? (r.user?.name ?? "Agent") : ticket.senderName;
|
||||
return `${sender}: ${r.body}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-5-nano"),
|
||||
system:
|
||||
"You are a helpful assistant that summarizes support ticket conversations. " +
|
||||
"Provide a clear, concise summary that captures the customer's issue, any actions taken, and the current status. " +
|
||||
"Keep the summary to 2-4 sentences. Return only the summary with no preamble.",
|
||||
prompt:
|
||||
`Subject: ${ticket.subject}\n\n` +
|
||||
`Customer message:\n${ticket.body}\n\n` +
|
||||
(conversation ? `Conversation:\n${conversation}` : "No replies yet."),
|
||||
});
|
||||
|
||||
res.json({ summary: text });
|
||||
});
|
||||
|
||||
router.post("/polish", requireAuth, async (req, res) => {
|
||||
const ticketId = parseId(req.params.ticketId);
|
||||
if (!ticketId) {
|
||||
|
|
@ -80,7 +122,7 @@ router.post("/polish", requireAuth, async (req, res) => {
|
|||
const customerName = ticket.senderName.split(" ")[0];
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
model: openai("gpt-5-nano"),
|
||||
system:
|
||||
"You are a helpful writing assistant for a customer support team. " +
|
||||
"Improve the given reply for clarity, professional tone, and grammar. " +
|
||||
|
|
|
|||
Loading…
Reference in a new issue