Add AI Quick Quote widget and inline customer reassignment
- New AI Quick Quote floating button: staff type a verbal description to get an instant price estimate for phone/walk-in customers; detected color names are fuzzy-matched against inventory for stock status; saves draft quote under a Walk-In / Phone customer with one click - Inline customer change on Quote Details and Job Details: always-visible native select with inline confirmation banner (no TomSelect dependency); ChangeCustomer AJAX endpoints on QuotesController and JobsController - Quote Edit page: customer dropdown is now editable (lock removed) - Fix AutoMapper missing CatalogCategory -> UpdateCategoryDto mapping that caused a crash on the catalog category Edit page - Help docs and AI knowledge base updated for all three features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -545,6 +545,37 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="changing-customer" class="mb-5">
|
||||
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-person-gear text-primary me-2"></i>Changing the Customer
|
||||
</h2>
|
||||
<p>
|
||||
The customer on a job can be changed at any time from the Job Details page — no need to
|
||||
delete and re-create the job. This is useful when:
|
||||
</p>
|
||||
<ul class="mb-3">
|
||||
<li class="mb-1">A job was created under the <em>Walk-In / Phone</em> placeholder and the real customer is added later.</li>
|
||||
<li class="mb-1">A job was accidentally assigned to the wrong customer.</li>
|
||||
<li class="mb-1">A job converted from a quote needs to be moved to a different customer record.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="h6 fw-semibold mt-3 mb-2">How to change the customer</h3>
|
||||
<ol class="mb-3">
|
||||
<li class="mb-2">Open the job from <strong>Operations › Jobs</strong> and go to its Details page.</li>
|
||||
<li class="mb-2">Find the <strong>Customer</strong> field in the job header — it appears as a dropdown showing the current customer.</li>
|
||||
<li class="mb-2">Select a different customer from the dropdown.</li>
|
||||
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> — click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert.</li>
|
||||
</ol>
|
||||
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
|
||||
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
The customer can also be changed on the <strong>Edit Job</strong> page using the Customer
|
||||
dropdown there. Any invoices or deposits already linked to the job are not automatically
|
||||
moved — update those separately if needed.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="blank-work-order" class="mb-5">
|
||||
<h2 class="h5 fw-semibold mb-3">Blank Work Order</h2>
|
||||
<p>
|
||||
@@ -611,6 +642,7 @@
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#shop-display-and-board">Shop Display & Priority Board</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#part-intake">Part Intake</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#shop-mobile">Shop Mobile</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#changing-customer">Changing the Customer</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#blank-work-order">Blank Work Order</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -343,6 +343,79 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="ai-quick-quote" class="mb-5">
|
||||
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-lightning-fill text-primary me-2"></i>AI Quick Quote
|
||||
</h2>
|
||||
<p>
|
||||
The <strong>AI Quick Quote</strong> widget lets you get an instant rough estimate from a verbal
|
||||
description — perfect for phone calls and walk-in customers when you don't have time to open the
|
||||
full quoting wizard. Look for the dark-blue floating button in the bottom-right corner of any page,
|
||||
just above the AI Help button.
|
||||
</p>
|
||||
|
||||
<h3 class="h6 fw-semibold mt-3 mb-2">How to use it</h3>
|
||||
<ol class="mb-3">
|
||||
<li class="mb-2">Click the <strong>AI Quick Quote</strong> floating button (bottom-right, dark blue with a lightning bolt icon).</li>
|
||||
<li class="mb-2">
|
||||
Type a description of the work — for example:<br>
|
||||
<em>"4 motorcycle wheels in Alien Silver with a black base coat, need sandblasting"</em>
|
||||
</li>
|
||||
<li class="mb-2">Set the <strong>Quantity</strong> and <strong>Number of Coats</strong>.</li>
|
||||
<li class="mb-2">Click <strong>Get Estimate</strong>. The AI analyses your description and returns a price estimate, estimated minutes, surface area, complexity rating, and a confidence score in just a few seconds.</li>
|
||||
<li class="mb-2">
|
||||
The panel also shows <strong>powder stock status</strong> for any color names detected in your description — a green check means you have it in stock, red means you don't, and a grey question mark means the system couldn't match it.
|
||||
</li>
|
||||
<li class="mb-2">If the estimate looks right, enter an optional <strong>Customer Reference</strong> (e.g., the caller's name) and click <strong>Save as Draft Quote</strong>. You land directly on the new quote's Details page where you can adjust anything and assign the real customer.</li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-permanent alert-info d-flex gap-2 mb-3" role="alert">
|
||||
<i class="bi bi-lightbulb-fill flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
Quick quotes are saved under a <strong>"Walk-In / Phone"</strong> customer so nothing blocks you from saving immediately. Once you have the customer's information, reassign the quote using the Customer dropdown on the Quote Details page — see <em>Changing the Customer</em> below.
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-permanent alert-warning d-flex gap-2 mb-0" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
The Quick Quote gives a rough estimate only — it uses your shop's operating costs and the AI's
|
||||
interpretation of your description. For formal quotes that will be sent to a customer, always
|
||||
open the quote and verify the details using the full item wizard before sending.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="changing-customer" class="mb-5">
|
||||
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-person-gear text-primary me-2"></i>Changing the Customer
|
||||
</h2>
|
||||
<p>
|
||||
The customer on a quote can be changed at any time from the Quote Details page — no need to
|
||||
delete and re-create the quote. This is particularly useful when:
|
||||
</p>
|
||||
<ul class="mb-3">
|
||||
<li class="mb-1">A quote was saved under the <em>Walk-In / Phone</em> placeholder and the real customer record is created later.</li>
|
||||
<li class="mb-1">A quote was accidentally assigned to the wrong customer.</li>
|
||||
<li class="mb-1">A prospect quote needs to be reassigned after the prospect becomes a customer.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="h6 fw-semibold mt-3 mb-2">How to change the customer</h3>
|
||||
<ol class="mb-3">
|
||||
<li class="mb-2">Open the quote from <strong>Operations › Quotes</strong> and go to its Details page.</li>
|
||||
<li class="mb-2">Find the <strong>Customer</strong> field in the quote header — it appears as a dropdown showing the current customer.</li>
|
||||
<li class="mb-2">Select a different customer from the dropdown.</li>
|
||||
<li class="mb-2">A confirmation banner appears: <em>"Change customer to [Name]?"</em> — click <strong>Save</strong> to confirm or <strong>Cancel</strong> to revert to the original.</li>
|
||||
</ol>
|
||||
<div class="alert alert-permanent alert-info d-flex gap-2 mb-0" role="alert">
|
||||
<i class="bi bi-info-circle flex-shrink-0 mt-1"></i>
|
||||
<div>
|
||||
The customer can also be changed on the <strong>Edit Quote</strong> page using the Customer
|
||||
dropdown there. If the quote was originally for a prospect, switching to a customer record
|
||||
automatically clears the prospect fields.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pricing-breakdown" class="mb-5">
|
||||
<h2 class="h4 fw-bold border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-bar-chart text-primary me-2"></i>Understanding the Pricing Breakdown
|
||||
@@ -415,6 +488,8 @@
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#prospect-conversion">Converting a Prospect</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#customer-approval-portal">Approval Portal</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#deposits">Deposits</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#ai-quick-quote">AI Quick Quote</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#changing-customer">Changing the Customer</a>
|
||||
<a class="nav-link py-1 px-3 small text-body" href="#pricing-breakdown">Pricing Breakdown</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -56,23 +56,22 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Customer</label>
|
||||
<p class="mb-0">
|
||||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@Model.CustomerId" class="text-decoration-none">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerCompanyName))
|
||||
@Html.AntiForgeryToken()
|
||||
<div data-cc-wrap data-cc-id="@Model.Id"
|
||||
data-cc-url="@Url.Action("ChangeCustomer", "Jobs")">
|
||||
<select class="form-select form-select-sm cc-select" style="max-width:300px;">
|
||||
@foreach (var c in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.CustomerSelectList)
|
||||
{
|
||||
<strong>@Model.CustomerCompanyName</strong>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerContactName))
|
||||
{
|
||||
<br />
|
||||
<small class="text-muted">@Model.CustomerContactName</small>
|
||||
}
|
||||
<option value="@c.Value" selected="@(c.Value == Model.CustomerId.ToString() ? "selected" : null)">@c.Text</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.CustomerName
|
||||
}
|
||||
</a>
|
||||
</p>
|
||||
</select>
|
||||
<div class="cc-confirm-banner d-none mt-2 p-2 bg-light border rounded d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="cc-confirm-text small fw-semibold"></span>
|
||||
<button type="button" class="btn btn-success btn-sm" data-cc-save>Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-cc-cancel>Cancel</button>
|
||||
</div>
|
||||
<div class="cc-error text-danger small mt-1 d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small mb-1">Priority</label>
|
||||
@@ -2140,6 +2139,7 @@
|
||||
@section Scripts {
|
||||
<link rel="stylesheet" href="~/css/job-photos.css" />
|
||||
<script src="~/js/job-photos.js" asp-append-version="true"></script>
|
||||
<script src="~/js/customer-change.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
// ── Inline date editing ──────────────────────────────────────────────
|
||||
const jobId = @Model.Id;
|
||||
|
||||
@@ -80,12 +80,24 @@
|
||||
else
|
||||
{
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
<strong>Customer:</strong>
|
||||
<a asp-controller="Customers" asp-action="Details" asp-route-id="@Model.CustomerId">
|
||||
@Model.CustomerName
|
||||
</a>
|
||||
</p>
|
||||
@Html.AntiForgeryToken()
|
||||
<strong>Customer:</strong>
|
||||
<div data-cc-wrap data-cc-id="@Model.Id"
|
||||
data-cc-url="@Url.Action("ChangeCustomer", "Quotes")"
|
||||
class="d-inline-block ms-1">
|
||||
<select class="form-select form-select-sm cc-select" style="max-width:300px;">
|
||||
@foreach (var c in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.CustomerSelectList)
|
||||
{
|
||||
<option value="@c.Value" selected="@(c.Value == Model.CustomerId.ToString() ? "selected" : null)">@c.Text</option>
|
||||
}
|
||||
</select>
|
||||
<div class="cc-confirm-banner d-none mt-2 p-2 bg-light border rounded d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="cc-confirm-text small fw-semibold"></span>
|
||||
<button type="button" class="btn btn-success btn-sm" data-cc-save>Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-cc-cancel>Cancel</button>
|
||||
</div>
|
||||
<div class="cc-error text-danger small mt-1 d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -2030,6 +2042,7 @@
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/customer-change.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
function resendQuote(quoteId) {
|
||||
// Reset modal state
|
||||
|
||||
@@ -23,14 +23,13 @@
|
||||
<input type="hidden" asp-for="QuoteStatusId" />
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<!-- Section 1: Customer / Prospect/Walk-In (Read-Only) -->
|
||||
<!-- Section 1: Customer / Prospect/Walk-In -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-person-circle me-2"></i>Customer / Prospect/Walk-In</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="hidden" asp-for="IsForProspect" />
|
||||
<input type="hidden" asp-for="CustomerId" />
|
||||
|
||||
@if (Model.IsForProspect)
|
||||
{
|
||||
@@ -78,13 +77,13 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Existing Customer (Read-Only Display) -->
|
||||
<div class="alert alert-light alert-permanent border mb-0 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-building text-success fs-5"></i>
|
||||
<div>
|
||||
<span class="fw-semibold">@ViewBag.CustomerName</span>
|
||||
<span class="text-muted ms-2 small">Customer cannot be changed after quote creation.</span>
|
||||
</div>
|
||||
<!-- Customer Dropdown (now editable) -->
|
||||
<div class="col-md-6">
|
||||
<label asp-for="CustomerId" class="form-label fw-semibold">Customer</label>
|
||||
<select asp-for="CustomerId" asp-items="ViewBag.Customers" id="customerSelect" class="form-select">
|
||||
<option value="">-- Select Customer --</option>
|
||||
</select>
|
||||
<span asp-validation-for="CustomerId" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -637,6 +636,8 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initTagInput('quoteTags', 'quoteTagsContainer');
|
||||
var custEl = document.getElementById('customerSelect');
|
||||
if (custEl) new TomSelect(custEl, { placeholder: '-- Select Customer --', openOnFocus: true, maxOptions: false });
|
||||
});
|
||||
|
||||
// Discount type toggle
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var token = Antiforgery.GetAndStoreTokens(Context).RequestToken;
|
||||
}
|
||||
|
||||
<!-- AI Quick Quote Widget -->
|
||||
<div id="qq-widget" class="qq-widget" aria-live="polite" aria-label="AI Quick Quote">
|
||||
|
||||
<!-- Trigger button -->
|
||||
<button id="qq-btn" class="qq-trigger" title="Get a quick phone estimate" aria-label="Open AI Quick Quote">
|
||||
<i class="bi bi-lightning-charge-fill fs-5"></i>
|
||||
<span class="qq-label">Quick Quote</span>
|
||||
</button>
|
||||
|
||||
<!-- Panel -->
|
||||
<div id="qq-panel" class="qq-panel" role="dialog" aria-modal="true" aria-label="AI Quick Quote" hidden>
|
||||
<div class="qq-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-lightning-charge-fill text-warning"></i>
|
||||
<span class="fw-semibold">AI Quick Quote</span>
|
||||
<span class="badge bg-secondary" style="font-size:0.65rem;">Beta</span>
|
||||
</div>
|
||||
<button id="qq-close" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Close" aria-label="Close Quick Quote">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Input -->
|
||||
<div id="qq-step-input" class="qq-body">
|
||||
<p class="text-muted mb-3" style="font-size:0.82rem;">
|
||||
Describe what the customer needs and get an instant estimate.
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" style="font-size:0.85rem;" for="qq-description">
|
||||
What does the customer have?
|
||||
</label>
|
||||
<textarea id="qq-description"
|
||||
class="form-control form-control-sm"
|
||||
rows="4"
|
||||
maxlength="600"
|
||||
placeholder="e.g. 4 car wheels done in Alexandrite with an Alien Silver base, about 18 inch diameter…"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-qty">Quantity</label>
|
||||
<input type="number" id="qq-qty" class="form-control form-control-sm" value="1" min="1" max="999" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-coats">Coats</label>
|
||||
<input type="number" id="qq-coats" class="form-control form-control-sm" value="1" min="1" max="5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="qq-input-error" class="alert alert-danger alert-permanent d-none py-2" style="font-size:0.82rem;"></div>
|
||||
|
||||
<button id="qq-analyze-btn" class="btn btn-primary w-100">
|
||||
<i class="bi bi-lightning-charge-fill me-1"></i> Get Estimate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Results -->
|
||||
<div id="qq-step-results" class="qq-body d-none">
|
||||
<!-- AI estimates -->
|
||||
<div id="qq-result-card" class="card border-0 bg-light mb-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="fw-semibold mb-2" id="qq-res-description" style="font-size:0.9rem;"></div>
|
||||
|
||||
<div class="row g-2 text-center mb-2">
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Sq Ft</div>
|
||||
<div class="fw-semibold" id="qq-res-sqft"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Complexity</div>
|
||||
<div class="fw-semibold" id="qq-res-complexity"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="small text-muted">Labor</div>
|
||||
<div class="fw-semibold" id="qq-res-minutes"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center border-top pt-2">
|
||||
<div>
|
||||
<div class="small text-muted">Estimate</div>
|
||||
<div class="fs-5 fw-bold text-success" id="qq-res-price"></div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="small text-muted">Confidence</div>
|
||||
<span id="qq-res-confidence" class="badge"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="qq-res-reasoning" class="mt-2 text-muted" style="font-size:0.78rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Powder / color stock status -->
|
||||
<div id="qq-powder-section" class="d-none mb-3">
|
||||
<div class="fw-semibold mb-1" style="font-size:0.82rem;">Powder Stock</div>
|
||||
<div id="qq-powder-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Save inputs -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label" style="font-size:0.82rem;" for="qq-reference">
|
||||
Reference <span class="text-muted">(caller name or memo)</span>
|
||||
</label>
|
||||
<input type="text" id="qq-reference" class="form-control form-control-sm"
|
||||
placeholder="e.g. John – 4 wheels" maxlength="100" />
|
||||
</div>
|
||||
|
||||
<div id="qq-save-error" class="alert alert-danger alert-permanent d-none py-2 mb-2" style="font-size:0.82rem;"></div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="qq-back-btn" class="btn btn-outline-secondary btn-sm flex-shrink-0">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</button>
|
||||
<button id="qq-save-btn" class="btn btn-success btn-sm flex-grow-1">
|
||||
<i class="bi bi-floppy me-1"></i> Save as Draft Quote
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing indicator (shared between steps) -->
|
||||
<div id="qq-loading" class="qq-loading d-none">
|
||||
<div class="qq-typing-dot"></div>
|
||||
<div class="qq-typing-dot"></div>
|
||||
<div class="qq-typing-dot"></div>
|
||||
<span class="ms-2 text-muted" style="font-size:0.82rem;">Analyzing…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="qq-token" value="@token" />
|
||||
|
||||
<script src="~/js/ai-quick-quote.js" asp-append-version="true"></script>
|
||||
|
||||
<style>
|
||||
.qq-widget {
|
||||
position: fixed;
|
||||
bottom: 134px; /* sits above the AI Help button at 80px */
|
||||
right: 24px;
|
||||
z-index: 1050;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.qq-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #1e3a8a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 10px 18px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.qq-trigger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.4);
|
||||
background: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.qq-panel {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 0;
|
||||
width: 380px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bs-body-bg, #fff);
|
||||
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qq-panel[hidden] { display: none !important; }
|
||||
|
||||
.qq-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: var(--bs-secondary-bg, #f8f9fa);
|
||||
border-bottom: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-body {
|
||||
padding: 14px;
|
||||
overflow-y: auto;
|
||||
max-height: 480px;
|
||||
}
|
||||
|
||||
.qq-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--bs-border-color, #dee2e6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-typing-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--bs-secondary-color, #6c757d);
|
||||
animation: qq-bounce 1.2s infinite ease-in-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qq-typing-dot:nth-child(2) { animation-delay: 0.2s; margin: 0 4px; }
|
||||
.qq-typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@@keyframes qq-bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.qq-powder-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.78rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
@@media (max-width: 480px) {
|
||||
.qq-widget { bottom: 80px; right: 16px; }
|
||||
.qq-panel { width: calc(100vw - 32px); right: 0; bottom: 54px; }
|
||||
.qq-label { display: none; }
|
||||
.qq-trigger { padding: 10px 14px; }
|
||||
}
|
||||
</style>
|
||||
@@ -2089,6 +2089,7 @@
|
||||
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
@await Html.PartialAsync("_AiQuickQuoteWidget")
|
||||
@await Html.PartialAsync("_AiHelpWidget")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user