mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 15:10:13 -05:00
Compare commits
31 Commits
deploy/age
...
205c9dfc99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205c9dfc99 | ||
|
|
231378d647 | ||
|
|
2cb11b0a67 | ||
|
|
ca5b5b49ad | ||
|
|
2cfd49fbb9 | ||
|
|
e68910c5c2 | ||
|
|
a0b9354e78 | ||
|
|
8dbecce34a | ||
|
|
d3bad68c80 | ||
|
|
e7fe274bda | ||
|
|
e28f42076f | ||
|
|
ad8491184b | ||
|
|
b56fed338a | ||
|
|
f60eb39e4c | ||
|
|
4c61b36233 | ||
|
|
ccbbf9161c | ||
|
|
39146f7b20 | ||
|
|
16513a340c | ||
|
|
30b9295156 | ||
|
|
3d0ff9463f | ||
|
|
b893d2df82 | ||
|
|
79b6117d80 | ||
|
|
d2ef434dec | ||
|
|
aaf83c2b4c | ||
|
|
d898bcff90 | ||
|
|
b4cf146c85 | ||
|
|
f21782a9a3 | ||
|
|
e4455987e7 | ||
|
|
b2ceb41dd6 | ||
|
|
f614153f30 | ||
|
|
8ca020e179 |
229
MERGE_NOTES_HITL_WEB_SYNC.md
Normal file
229
MERGE_NOTES_HITL_WEB_SYNC.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# HITL Web Integration Merge Notes
|
||||
|
||||
Date: 2026-02-06
|
||||
Scope: Frontend (`web/`) integration between `origin/build/feat/hitl` and `origin/feat/support-agent-sandbox`
|
||||
|
||||
## 1. Context and Goal
|
||||
|
||||
This merge effort combines two large frontend change streams:
|
||||
|
||||
- `origin/build/feat/hitl`: Human-in-the-Loop (HITL) capabilities, workflow pause/resume, human input forms, related UI/events/types.
|
||||
- `origin/feat/support-agent-sandbox`: Sandbox and frontend refactors, including structural changes in workflow debug/preview hooks and shared runtime paths.
|
||||
|
||||
Primary objective:
|
||||
|
||||
- Integrate `web/` safely without HITL regression.
|
||||
- Keep backend/API conflict handling isolated from product branches where possible.
|
||||
- Preserve both branches’ effective frontend logic, especially around streaming events and workflow state transitions.
|
||||
|
||||
## 2. Current Integration Snapshot
|
||||
|
||||
Current integration branch: `wip/hitl-merge-web-conflicts-20260206-175434`
|
||||
|
||||
Current `web/` merge footprint:
|
||||
|
||||
- Total changed files in index: `266`
|
||||
- Added: `79`
|
||||
- Modified: `183`
|
||||
- Deleted: `4`
|
||||
- Files modified by both branches (overlap hotspot): `81`
|
||||
|
||||
High-level distribution (approximate by path category):
|
||||
|
||||
- `i18n`: `66`
|
||||
- `workflow/nodes/human-input`: `28`
|
||||
- `workflow` other files: `46`
|
||||
- `workflow/hooks/use-workflow-run-event`: `9`
|
||||
- `base/chat`: `26`
|
||||
- `base/prompt-editor`: `23`
|
||||
- `workflow/panel/debug-and-preview`: `7`
|
||||
- `service`: `8`
|
||||
- `icons`: `11`
|
||||
- `eslint-suppressions.json`: `1`
|
||||
|
||||
## 3. What This Merge Contains (Functional Context)
|
||||
|
||||
Core merge content across frontend:
|
||||
|
||||
- New HITL workflow node surface:
|
||||
- New Human Input node implementation and panel components.
|
||||
- Delivery methods (WebApp/Email and related UI configuration paths).
|
||||
- Workflow run event pipeline:
|
||||
- Added handling for `human_input_required`, `human_input_form_filled`, `human_input_form_timeout`, `workflow_paused`.
|
||||
- Added/updated run-event hooks and workflow state propagation.
|
||||
- Debug/preview refactor integration:
|
||||
- Existing refactor into smaller hooks was retained.
|
||||
- Incoming HITL logic from the other branch was migrated into the new structure.
|
||||
- Chat rendering and interaction:
|
||||
- Human input form list and submitted-form list rendering in result views.
|
||||
- Pause-aware behavior in workflow/chat result components.
|
||||
- Prompt editor support:
|
||||
- HITL input block support and request URL block integration.
|
||||
- Service/runtime updates:
|
||||
- Stream parser and callback type expansion in `web/service/base.ts`.
|
||||
- HITL form submission service endpoints in `web/service/workflow.ts`.
|
||||
- Supporting assets:
|
||||
- HITL icon assets and references.
|
||||
- Multi-locale translation updates for workflow/common/share strings.
|
||||
|
||||
## 4. Hardening Patches Added After Review
|
||||
|
||||
After conflict resolution, additional safety fixes were applied to avoid HITL degradation:
|
||||
|
||||
1. Guard `findIndex` before `splice` in form-filled handlers
|
||||
- Prevent accidental `splice(-1, 1)` that could remove the wrong form.
|
||||
- Files:
|
||||
- `web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-filled.ts`
|
||||
- `web/app/components/base/chat/chat/hooks.ts`
|
||||
|
||||
2. Guard `findIndex` before timeout field update
|
||||
- Prevent out-of-bounds access when timeout event arrives for non-existing local entry.
|
||||
- Files:
|
||||
- `web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-human-input-form-timeout.ts`
|
||||
- `web/app/components/base/chat/chat/hooks.ts`
|
||||
|
||||
3. Fix tracing index condition for node-started event
|
||||
- Correct handling when `currentIndex === 0` to avoid duplicated tracing entries.
|
||||
- File:
|
||||
- `web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-started.ts`
|
||||
|
||||
4. Fix trigger-run-all input validation
|
||||
- Correct empty-array guard for `TriggerType.All`.
|
||||
- File:
|
||||
- `web/app/components/workflow-app/hooks/use-workflow-run.ts`
|
||||
|
||||
These are behavior-preserving safeguards: they do not change intended flow, only remove edge-case breakage.
|
||||
|
||||
## 5. Recommended Merge Strategy (Relay Branch Model)
|
||||
|
||||
This strategy is feasible and stable for this responsibility.
|
||||
|
||||
Core principle:
|
||||
|
||||
- Treat integration as a dedicated frontend relay branch.
|
||||
- Do not directly merge this integration branch back into `feat/support-agent-sandbox`.
|
||||
|
||||
### Step-by-step process
|
||||
|
||||
1. Create and keep a fixed relay branch (example: `int/hitl-web-sync`).
|
||||
2. On relay branch, repeat:
|
||||
- Merge `origin/build/feat/hitl`.
|
||||
- Merge `origin/feat/support-agent-sandbox`.
|
||||
- Resolve only `web/` conflicts as the business target.
|
||||
- Avoid carrying backend conflict resolutions into product branches.
|
||||
3. Commit every loop iteration so the relay branch is always checkout-able and buildable.
|
||||
4. Before exporting result, merge latest `feat/support-agent-sandbox` into relay one more time to avoid rollback of recent sandbox frontend changes.
|
||||
5. Export only `web/` back to sandbox branch:
|
||||
- `git checkout int/hitl-web-sync -- web`
|
||||
- or `git restore --source=int/hitl-web-sync --staged --worktree web`
|
||||
- create one alignment commit in `feat/support-agent-sandbox`.
|
||||
|
||||
## 6. Why This Strategy Works
|
||||
|
||||
- Backend conflicts stay isolated in relay workflow and do not block sandbox branch progress.
|
||||
- Future merge to `main` should mostly contain incremental conflicts instead of re-opening this large web integration.
|
||||
- Squash merge on HITL branch is acceptable; Git merge correctness is content-based, not commit-lineage-based.
|
||||
|
||||
## 7. Validation Gates Per Iteration
|
||||
|
||||
Run these checks in each relay cycle:
|
||||
|
||||
1. Conflict sanity:
|
||||
- `git diff --name-only --diff-filter=U -- web`
|
||||
- `rg "^(<<<<<<<|=======|>>>>>>>)" web`
|
||||
|
||||
2. Type/lint sanity:
|
||||
- `cd web && pnpm type-check:tsgo`
|
||||
- `cd web && pnpm eslint <targeted-hitl-files> --cache --concurrency=auto`
|
||||
|
||||
3. Targeted tests (at minimum):
|
||||
- Prompt editor shortcuts plugin tests.
|
||||
- Workflow/rag pipeline tests impacted by this merge.
|
||||
- HITL-specific unit tests once stabilized.
|
||||
|
||||
4. Functional smoke focus:
|
||||
- Workflow pause/resume event chain.
|
||||
- Human input required -> submit -> continue path.
|
||||
- Human input timeout rendering/update path.
|
||||
- Debug-and-preview flow plus chat-with-history/embedded-chat wrappers.
|
||||
|
||||
## 8. Open Follow-ups
|
||||
|
||||
1. Add explicit unit tests for index-guard behavior in HITL handlers:
|
||||
- form filled when node id is missing.
|
||||
- form timeout when node id is missing.
|
||||
- node-started update when tracing index is `0`.
|
||||
|
||||
2. Resolve existing repository-level type-check blocker unrelated to this patch:
|
||||
- `web/app/components/base/chat/chat/hooks.hitl.spec.tsx` currently reports argument-count mismatch.
|
||||
|
||||
3. Keep translation synchronization process clear:
|
||||
- English keys are the source of truth.
|
||||
- Non-English locale completion can follow in separate localization pass.
|
||||
|
||||
4. If `web/eslint-suppressions.json` grows during conflict loops, regenerate in a dedicated cleanup commit.
|
||||
|
||||
## 9. Final Promotion Plan to Mainline
|
||||
|
||||
When HITL is finalized:
|
||||
|
||||
1. Refresh relay branch from latest sandbox and latest HITL source.
|
||||
2. Re-run the validation gates.
|
||||
3. Export `web/` only to `feat/support-agent-sandbox` in one commit.
|
||||
4. Merge sandbox branch to `main` with normal review.
|
||||
5. Handle only net-new conflicts introduced after this integration window.
|
||||
|
||||
## 10. Operational Rule Summary
|
||||
|
||||
- Use `int/hitl-web-sync` as the long-lived integration relay.
|
||||
- Keep merges frequent and small.
|
||||
- Keep commits incremental and reproducible.
|
||||
- Keep HITL event/data flow as non-regression priority.
|
||||
- Export frontend result as content-only alignment (`web/`) to sandbox branch.
|
||||
|
||||
## 11. Mainline Merge Replay Risk and Web Preservation Procedure (2026-02-07)
|
||||
|
||||
### Verified conclusion
|
||||
|
||||
Even if `web/` content is already aligned, Git can still replay frontend conflicts in a later merge to `main` if the branch only copied files but did not record merge ancestry with HITL.
|
||||
|
||||
Observed in local probes:
|
||||
|
||||
1. `origin/feat/support-agent-sandbox` + merge `origin/build/feat/hitl`:
|
||||
- `web` conflicts: `24`
|
||||
2. `pre-align-hitl-frontend` + merge `origin/build/feat/hitl`:
|
||||
- `web` conflicts: `0`
|
||||
3. Sandbox branch with one `web` alignment commit (`git checkout pre-align-hitl-frontend -- web`) + merge `origin/build/feat/hitl`:
|
||||
- `web` conflicts: `31`
|
||||
|
||||
Interpretation:
|
||||
|
||||
- File-content alignment commit is not equivalent to merge-lineage alignment.
|
||||
- `git checkout --ours -- web` resolves conflicting paths only, but does not revert auto-merged non-conflict web changes.
|
||||
|
||||
### Recommended operational flow when backend later merges `main`
|
||||
|
||||
Preconditions:
|
||||
|
||||
1. Freeze a stable window: no new commits on `main` and sandbox during operation.
|
||||
2. Update `pre-align-hitl-frontend` by merging latest `main` and latest sandbox first.
|
||||
3. Export `web/` from `pre-align-hitl-frontend` to sandbox and commit one explicit alignment commit.
|
||||
|
||||
When backend branch performs `git merge origin/main`:
|
||||
|
||||
1. If the decision is to keep current branch's frontend completely, restore `web/` to pre-merge `HEAD`:
|
||||
- `git restore --source=HEAD --staged --worktree -- web`
|
||||
2. If restore is blocked by unmerged `modify/delete` entries, use fallback:
|
||||
- `git checkout HEAD -- web`
|
||||
- resolve remaining unmerged web paths to ours (for delete-on-ours case: `git rm <path>`)
|
||||
- `git restore --source=HEAD --staged --worktree --no-overlay -- web`
|
||||
3. Confirm `web/` is fully unchanged in merge state:
|
||||
- `git status --short -- web` should be empty
|
||||
- `git diff --name-only --diff-filter=U -- web` should be empty
|
||||
- `git diff --cached --name-only -- web` should be empty
|
||||
- `git diff --name-only -- web` should be empty
|
||||
4. Continue resolving only non-web conflicts and finish merge commit.
|
||||
|
||||
### Practical rule
|
||||
|
||||
If the intent is "this merge should not change frontend", enforce it with `restore` to `HEAD` on `web/` rather than relying only on `checkout --ours -- web`.
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Result from './content'
|
||||
|
||||
// Only mock react-i18next for translations
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock copy-to-clipboard for the Header component
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: vi.fn(() => true),
|
||||
}))
|
||||
|
||||
// Mock the format function from service/base
|
||||
vi.mock('@/service/base', () => ({
|
||||
format: (content: string) => content.replace(/\n/g, '<br>'),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('Result (content)', () => {
|
||||
const mockOnFeedback = vi.fn()
|
||||
|
||||
const defaultProps = {
|
||||
content: 'Test content here',
|
||||
showFeedback: true,
|
||||
feedback: { rating: null } as FeedbackType,
|
||||
onFeedback: mockOnFeedback,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the Header component', () => {
|
||||
render(<Result {...defaultProps} />)
|
||||
|
||||
// Header renders the result title
|
||||
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render content', () => {
|
||||
render(<Result {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('Test content here')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render formatted content with line breaks', () => {
|
||||
render(
|
||||
<Result
|
||||
{...defaultProps}
|
||||
content={'Line 1\nLine 2'}
|
||||
/>,
|
||||
)
|
||||
|
||||
// The format function converts \n to <br>
|
||||
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
|
||||
expect(contentDiv?.innerHTML).toContain('Line 1<br>Line 2')
|
||||
})
|
||||
|
||||
it('should have max height style', () => {
|
||||
render(<Result {...defaultProps} />)
|
||||
|
||||
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
|
||||
expect(contentDiv).toHaveStyle({ maxHeight: '70vh' })
|
||||
})
|
||||
|
||||
it('should render with empty content', () => {
|
||||
render(
|
||||
<Result
|
||||
{...defaultProps}
|
||||
content=""
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with HTML content safely', () => {
|
||||
render(
|
||||
<Result
|
||||
{...defaultProps}
|
||||
content="<script>alert('xss')</script>"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Content is rendered via dangerouslySetInnerHTML
|
||||
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
|
||||
expect(contentDiv).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('feedback props', () => {
|
||||
it('should pass showFeedback to Header', () => {
|
||||
render(
|
||||
<Result
|
||||
{...defaultProps}
|
||||
showFeedback={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Feedback buttons should not be visible
|
||||
const feedbackArea = document.querySelector('[class*="space-x-1 rounded-lg border"]')
|
||||
expect(feedbackArea).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass feedback to Header', () => {
|
||||
render(
|
||||
<Result
|
||||
{...defaultProps}
|
||||
feedback={{ rating: 'like' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Like button should be highlighted
|
||||
const likeButton = document.querySelector('[class*="primary"]')
|
||||
expect(likeButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
expect((Result as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import * as React from 'react'
|
||||
import { format } from '@/service/base'
|
||||
import Header from './header'
|
||||
|
||||
export type IResultProps = {
|
||||
content: string
|
||||
showFeedback: boolean
|
||||
feedback: FeedbackType
|
||||
onFeedback: (feedback: FeedbackType) => void
|
||||
}
|
||||
const Result: FC<IResultProps> = ({
|
||||
content,
|
||||
showFeedback,
|
||||
feedback,
|
||||
onFeedback,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-max basis-3/4">
|
||||
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
|
||||
<div
|
||||
className="mt-4 flex w-full overflow-scroll text-sm font-normal leading-5 text-gray-900"
|
||||
style={{
|
||||
maxHeight: '70vh',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(content),
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
@@ -1,176 +0,0 @@
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Header from './header'
|
||||
|
||||
// Only mock react-i18next for translations
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock copy-to-clipboard
|
||||
const mockCopy = vi.fn((_text: string) => true)
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: (text: string) => mockCopy(text),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('Header', () => {
|
||||
const mockOnFeedback = vi.fn()
|
||||
|
||||
const defaultProps = {
|
||||
result: 'Test result content',
|
||||
showFeedback: true,
|
||||
feedback: { rating: null } as FeedbackType,
|
||||
onFeedback: mockOnFeedback,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the result title', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the copy button', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('generation.copy')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy functionality', () => {
|
||||
it('should copy result when copy button is clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
const copyButton = screen.getByText('generation.copy').closest('button')
|
||||
fireEvent.click(copyButton!)
|
||||
|
||||
expect(mockCopy).toHaveBeenCalledWith('Test result content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('feedback buttons when showFeedback is true', () => {
|
||||
it('should show feedback buttons when no rating is given', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
// Should show both thumbs up and down buttons
|
||||
const buttons = document.querySelectorAll('[class*="cursor-pointer"]')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should show like button highlighted when rating is like', () => {
|
||||
render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
feedback={{ rating: 'like' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should show the undo button for like
|
||||
const likeButton = document.querySelector('[class*="primary"]')
|
||||
expect(likeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show dislike button highlighted when rating is dislike', () => {
|
||||
render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
feedback={{ rating: 'dislike' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should show the undo button for dislike
|
||||
const dislikeButton = document.querySelector('[class*="red"]')
|
||||
expect(dislikeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onFeedback with like when thumbs up is clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
// Find the thumbs up button (first one in the feedback area)
|
||||
const thumbButtons = document.querySelectorAll('[class*="cursor-pointer"]')
|
||||
const thumbsUp = Array.from(thumbButtons).find(btn =>
|
||||
btn.className.includes('rounded-md') && !btn.className.includes('primary'),
|
||||
)
|
||||
|
||||
if (thumbsUp) {
|
||||
fireEvent.click(thumbsUp)
|
||||
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: 'like' })
|
||||
}
|
||||
})
|
||||
|
||||
it('should call onFeedback with dislike when thumbs down is clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
|
||||
// Find the thumbs down button
|
||||
const thumbButtons = document.querySelectorAll('[class*="cursor-pointer"]')
|
||||
const thumbsDown = Array.from(thumbButtons).pop()
|
||||
|
||||
if (thumbsDown) {
|
||||
fireEvent.click(thumbsDown)
|
||||
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: 'dislike' })
|
||||
}
|
||||
})
|
||||
|
||||
it('should call onFeedback with null when undo like is clicked', () => {
|
||||
render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
feedback={{ rating: 'like' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// When liked, clicking the like button again should undo it (has bg-primary-100 class)
|
||||
const likeButton = document.querySelector('[class*="bg-primary-100"]')
|
||||
expect(likeButton).toBeInTheDocument()
|
||||
fireEvent.click(likeButton!)
|
||||
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: null })
|
||||
})
|
||||
|
||||
it('should call onFeedback with null when undo dislike is clicked', () => {
|
||||
render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
feedback={{ rating: 'dislike' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// When disliked, clicking the dislike button again should undo it (has bg-red-100 class)
|
||||
const dislikeButton = document.querySelector('[class*="bg-red-100"]')
|
||||
expect(dislikeButton).toBeInTheDocument()
|
||||
fireEvent.click(dislikeButton!)
|
||||
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('feedback buttons when showFeedback is false', () => {
|
||||
it('should not show feedback buttons', () => {
|
||||
render(
|
||||
<Header
|
||||
{...defaultProps}
|
||||
showFeedback={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should not show feedback area buttons (only copy button)
|
||||
const feedbackArea = document.querySelector('[class*="space-x-1 rounded-lg border"]')
|
||||
expect(feedbackArea).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
expect((Header as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,117 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type IResultHeaderProps = {
|
||||
result: string
|
||||
showFeedback: boolean
|
||||
feedback: FeedbackType
|
||||
onFeedback: (feedback: FeedbackType) => void
|
||||
}
|
||||
|
||||
const Header: FC<IResultHeaderProps> = ({
|
||||
feedback,
|
||||
showFeedback,
|
||||
onFeedback,
|
||||
result,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="text-2xl font-normal leading-4 text-gray-800">{t('generation.resultTitle', { ns: 'share' })}</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
className="h-7 p-[2px] pr-2"
|
||||
onClick={() => {
|
||||
copy(result)
|
||||
Toast.notify({ type: 'success', message: 'copied' })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ClipboardDocumentIcon className="mr-1 h-3 w-4 text-gray-500" />
|
||||
<span className="text-xs leading-3 text-gray-500">{t('generation.copy', { ns: 'share' })}</span>
|
||||
</>
|
||||
</Button>
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'like' && (
|
||||
<Tooltip
|
||||
popupContent="Undo Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-primary-200 bg-primary-100 !text-primary-600 hover:border-primary-300 hover:bg-primary-200"
|
||||
>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
|
||||
<Tooltip
|
||||
popupContent="Undo Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-red-200 bg-red-100 !text-red-600 hover:border-red-300 hover:bg-red-200"
|
||||
>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && !feedback.rating && (
|
||||
<div className="flex space-x-1 rounded-lg border border-gray-200 p-[1px]">
|
||||
<Tooltip
|
||||
popupContent="Great Rating"
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'like',
|
||||
})
|
||||
}}
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent="Undesirable Response"
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'dislike',
|
||||
})
|
||||
}}
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
Reference in New Issue
Block a user