copy job info
This commit is contained in:
parent
91b1f08000
commit
8c90506380
@ -6,6 +6,7 @@ import React from "react";
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
DollarSign,
|
||||
Download,
|
||||
ExternalLink,
|
||||
@ -15,10 +16,12 @@ import {
|
||||
RefreshCcw,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { copyTextToClipboard, formatJobForLlmContext } from "@client/lib/jobCopy";
|
||||
import type { Job } from "../../shared/types";
|
||||
import { ScoreIndicator } from "./ScoreIndicator";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
@ -68,6 +71,15 @@ export const JobCard: React.FC<JobCardProps> = ({
|
||||
const pdfHref = `/pdfs/resume_${job.id}.pdf`;
|
||||
const deadline = formatDate(job.deadline);
|
||||
|
||||
const handleCopyInfo = async () => {
|
||||
try {
|
||||
await copyTextToClipboard(formatJobForLlmContext(job));
|
||||
toast.success("Copied job info", { description: "LLM-ready context copied to clipboard." });
|
||||
} catch {
|
||||
toast.error("Could not copy job info");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-3">
|
||||
@ -132,6 +144,11 @@ export const JobCard: React.FC<JobCardProps> = ({
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={handleCopyInfo}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy info
|
||||
</Button>
|
||||
|
||||
{hasPdf && (
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a href={pdfHref} target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@ -8,12 +8,14 @@ import {
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
Download,
|
||||
ExternalLink,
|
||||
MoreHorizontal,
|
||||
RefreshCcw,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -27,6 +29,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { copyTextToClipboard, formatJobForLlmContext } from "@client/lib/jobCopy";
|
||||
import type { Job } from "../../shared/types";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
|
||||
@ -137,6 +140,15 @@ export const JobTable: React.FC<JobTableProps> = ({
|
||||
const allSelected = jobs.length > 0 && selectedCount === jobs.length;
|
||||
const someSelected = selectedCount > 0 && selectedCount < jobs.length;
|
||||
|
||||
const handleCopyInfo = async (job: Job) => {
|
||||
try {
|
||||
await copyTextToClipboard(formatJobForLlmContext(job));
|
||||
toast.success("Copied job info", { description: "LLM-ready context copied to clipboard." });
|
||||
} catch {
|
||||
toast.error("Could not copy job info");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
@ -273,6 +285,11 @@ export const JobTable: React.FC<JobTableProps> = ({
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={() => void handleCopyInfo(job)}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy info
|
||||
</DropdownMenuItem>
|
||||
|
||||
{hasPdf && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
|
||||
110
orchestrator/src/client/lib/jobCopy.ts
Normal file
110
orchestrator/src/client/lib/jobCopy.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import type { Job } from "@shared/types";
|
||||
|
||||
const pushLine = (lines: string[], label: string, value: unknown) => {
|
||||
if (value == null) return;
|
||||
const normalized = typeof value === "string" ? value.trim() : String(value);
|
||||
if (!normalized) return;
|
||||
lines.push(`${label}: ${normalized}`);
|
||||
};
|
||||
|
||||
const pushBlock = (lines: string[], heading: string, value: string | null | undefined) => {
|
||||
const normalized = value?.trim();
|
||||
if (!normalized) return;
|
||||
lines.push("");
|
||||
lines.push(`${heading}:`);
|
||||
lines.push(normalized);
|
||||
};
|
||||
|
||||
export const formatJobForLlmContext = (job: Job) => {
|
||||
const jobLink = job.applicationLink || job.jobUrl;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("JOB CONTEXT");
|
||||
|
||||
pushLine(lines, "Title", job.title);
|
||||
pushLine(lines, "Company", job.employer);
|
||||
pushLine(lines, "Source", job.source);
|
||||
pushLine(lines, "Status", job.status);
|
||||
|
||||
pushLine(lines, "Job URL", job.jobUrl);
|
||||
pushLine(lines, "Application link", job.applicationLink);
|
||||
pushLine(lines, "Best link", jobLink);
|
||||
pushLine(lines, "Direct URL", job.jobUrlDirect);
|
||||
pushLine(lines, "Source job id", job.sourceJobId);
|
||||
|
||||
pushLine(lines, "Location", job.location);
|
||||
pushLine(lines, "Remote", job.isRemote);
|
||||
pushLine(lines, "Disciplines", job.disciplines);
|
||||
pushLine(lines, "Job type", job.jobType);
|
||||
pushLine(lines, "Job level", job.jobLevel);
|
||||
pushLine(lines, "Job function", job.jobFunction);
|
||||
pushLine(lines, "Listing type", job.listingType);
|
||||
|
||||
pushLine(lines, "Salary", job.salary);
|
||||
if (job.salaryMinAmount != null || job.salaryMaxAmount != null) {
|
||||
pushLine(
|
||||
lines,
|
||||
"Salary range",
|
||||
[
|
||||
job.salaryMinAmount != null ? String(job.salaryMinAmount) : null,
|
||||
job.salaryMaxAmount != null ? String(job.salaryMaxAmount) : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" - "),
|
||||
);
|
||||
}
|
||||
pushLine(lines, "Salary interval", job.salaryInterval);
|
||||
pushLine(lines, "Salary currency", job.salaryCurrency);
|
||||
pushLine(lines, "Salary source", job.salarySource);
|
||||
|
||||
pushLine(lines, "Degree required", job.degreeRequired);
|
||||
pushLine(lines, "Starting", job.starting);
|
||||
pushLine(lines, "Deadline", job.deadline);
|
||||
pushLine(lines, "Date posted", job.datePosted);
|
||||
|
||||
pushLine(lines, "Skills", job.skills);
|
||||
pushLine(lines, "Experience", job.experienceRange);
|
||||
pushLine(lines, "Emails", job.emails);
|
||||
|
||||
pushLine(lines, "Company industry", job.companyIndustry);
|
||||
pushLine(lines, "Company URL", job.companyUrlDirect || job.employerUrl);
|
||||
pushLine(lines, "Company employees", job.companyNumEmployees);
|
||||
pushLine(lines, "Company revenue", job.companyRevenue);
|
||||
pushLine(lines, "Company rating", job.companyRating);
|
||||
pushLine(lines, "Company reviews", job.companyReviewsCount);
|
||||
pushLine(lines, "Company addresses", job.companyAddresses);
|
||||
|
||||
pushLine(lines, "Discovered", job.discoveredAt);
|
||||
pushLine(lines, "Processed", job.processedAt);
|
||||
|
||||
pushBlock(lines, "Job description", job.jobDescription);
|
||||
pushBlock(lines, "Company description", job.companyDescription);
|
||||
|
||||
return lines.join("\n").trim() + "\n";
|
||||
};
|
||||
|
||||
export async function copyTextToClipboard(text: string) {
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.left = "0";
|
||||
textarea.style.opacity = "0";
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
if (!ok) {
|
||||
throw new Error("Copy failed");
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user