Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
179610c
test(v21): establish bilingual search translation UX causal RED
laiqian0239-glitch Aug 15, 2026
267e971
fix(v21): register exact bilingual Product UX route
laiqian0239-glitch Aug 15, 2026
14e8c70
feat(v21): bridge existing bilingual search translation jobs
laiqian0239-glitch Aug 15, 2026
f0c1381
feat(v21): expose exact bilingual search translation bridge
laiqian0239-glitch Aug 15, 2026
f501570
feat(v21): type bilingual Product projections
laiqian0239-glitch Aug 15, 2026
414c965
feat(v21): project existing search translation authorities
laiqian0239-glitch Aug 15, 2026
ca01669
feat(v21): inject public Product navigation seam
laiqian0239-glitch Aug 15, 2026
a3cc33a
feat(v21): bind Product search to public Element navigation
laiqian0239-glitch Aug 15, 2026
f64cfaa
feat(v21): add bilingual search translation task surface
laiqian0239-glitch Aug 15, 2026
173bd42
feat(v21): compose bilingual search across Product scenes
laiqian0239-glitch Aug 15, 2026
423a0be
feat(v21): polish bilingual search with shared Product tokens
laiqian0239-glitch Aug 15, 2026
def0b6c
Merge fresh main into bilingual Product Experience
laiqian0239-glitch Aug 15, 2026
aea4b5f
Merge fresh main into bilingual Product Experience
laiqian0239-glitch Aug 15, 2026
89ea054
Merge fresh main into bilingual Product Experience
laiqian0239-glitch Aug 15, 2026
2535f32
Merge fresh main into bilingual Product Experience
laiqian0239-glitch Aug 15, 2026
21cf8fd
test(v21): lock bilingual translation freshness failures
laiqian0239-glitch Aug 15, 2026
faaef39
fix(v21): preserve bilingual translation freshness
laiqian0239-glitch Aug 15, 2026
5de339a
Merge trusted main into bilingual Product candidate
laiqian0239-glitch Aug 15, 2026
9c95ee2
Merge pull request #423 from laiqian0239-glitch/main
laiqian0239-glitch Aug 15, 2026
36e43c3
Merge pull request #426 from laiqian0239-glitch/main
laiqian0239-glitch Aug 15, 2026
0462ada
fix(v21): harden bilingual search task UX
laiqian0239-glitch Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ contextBridge.exposeInMainWorld('yanceDesktop', Object.freeze({
setActiveConversation: conversationId => ipcRenderer.invoke('desktop:set-active-conversation', { activeConversationId: String(conversationId || '') }),
storeSnapshot: input => invokeStore('store:get-snapshot', input),
storeSocialContext: input => invokeStore('store:get-social-context', input),
storeSearchWorkspace: input => invokeStore('store:search-workspace', input),
storeCreateTranslationJob: input => invokeStore('store:create-translation-job', input),
storeGetTranslationJob: input => invokeStore('store:get-translation-job', input),
storeCancelTranslationJob: input => invokeStore('store:cancel-translation-job', input),
storeRetryTranslationJob: input => invokeStore('store:retry-translation-job', input),
storeGenerateReply: (input, options = {}) => invokeStoreCancelable('store:generate-reply', input, options),
storeApproveReply: input => invokeStore('store:approve-reply', input),
storeRejectReply: input => invokeStore('store:reject-reply', input),
Expand Down
45 changes: 45 additions & 0 deletions electron/r32StoreBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
const CHANNELS = Object.freeze({
snapshot: 'store:get-snapshot',
socialContext: 'store:get-social-context',
searchWorkspace: 'store:search-workspace',
createTranslationJob: 'store:create-translation-job',
getTranslationJob: 'store:get-translation-job',
cancelTranslationJob: 'store:cancel-translation-job',
retryTranslationJob: 'store:retry-translation-job',
generateReply: 'store:generate-reply',
cancelRequest: 'store:cancel-request',
approveReply: 'store:approve-reply',
Expand All @@ -26,6 +31,15 @@ function jsonBody(value) {
return JSON.stringify(value || {});
}

function requiredIdentifier(value, name) {
const id = clean(value);
if (id) return id;
const error = new Error(`${name} is required`);
error.code = `${String(name || 'identifier').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}_REQUIRED`;
error.reasonCode = error.code;
throw error;
}

function serializeBridgeError(error) {
return {
__yanceBridgeError: true,
Expand Down Expand Up @@ -68,6 +82,37 @@ function installR32StoreBridge({ ipcMain, apiRequest }) {
});
return apiRequest(`/api/r32/store/customers/${encodeURIComponent(contactId)}/social-context?${query}`);
},
[CHANNELS.searchWorkspace]: (_event, input = {}) => {
const queryText = clean(input.query);
const limit = Math.max(1, Math.min(200, Number(input.limit || 80)));
const query = new URLSearchParams({ q: queryText, limit: String(limit) });
return apiRequest(`/api/r32/store/search?${query}`);
Comment thread
laiqian0239-glitch marked this conversation as resolved.
},
[CHANNELS.createTranslationJob]: (_event, input = {}) => {
const messageId = requiredIdentifier(input.messageId, 'messageId');
return apiRequest(`/api/r32/store/translations/messages/${encodeURIComponent(messageId)}/jobs`, {
method: 'POST',
body: jsonBody({
force: input.force === true,
forceNew: input.forceNew === true,
timeoutMs: input.timeoutMs
})
});
},
[CHANNELS.getTranslationJob]: (_event, input = {}) => {
const jobId = requiredIdentifier(input.jobId, 'jobId');
return apiRequest(`/api/r32/store/translations/jobs/${encodeURIComponent(jobId)}`);
},
[CHANNELS.cancelTranslationJob]: (_event, input = {}) => {
const jobId = requiredIdentifier(input.jobId, 'jobId');
return apiRequest(`/api/r32/store/translations/jobs/${encodeURIComponent(jobId)}`, { method: 'DELETE' });
},
[CHANNELS.retryTranslationJob]: (_event, input = {}) => {
const jobId = requiredIdentifier(input.jobId, 'jobId');
return apiRequest(`/api/r32/store/translations/jobs/${encodeURIComponent(jobId)}/retry`, {
method: 'POST', body: jsonBody({ timeoutMs: input.timeoutMs })
});
},
[CHANNELS.generateReply]: async (event, input = {}) => {
const requestId = clean(input.__yanceBridgeRequestId);
const body = { ...input };
Expand Down
9 changes: 7 additions & 2 deletions integration/element-module/src/YanceWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React from "react";
import { ProductExperienceShell } from "./product-experience/ProductExperienceShell";
import type { RelationshipProjection } from "./product-experience/experienceTypes";

export function YanceWorkspace(): React.JSX.Element {
return <ProductExperienceShell />;
type YanceWorkspaceProps = {
navigateSearchResult?: (relationship: RelationshipProjection) => Promise<boolean>;
};

export function YanceWorkspace({ navigateSearchResult }: YanceWorkspaceProps): React.JSX.Element {
return <ProductExperienceShell navigateSearchResult={navigateSearchResult} />;
}
29 changes: 28 additions & 1 deletion integration/element-module/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,41 @@ import React from "react";
import type { Api, Module, ModuleFactory } from "@element-hq/element-web-module-api";
import { YanceWorkspace } from "./YanceWorkspace";
import { ProductComposerAccessory } from "./product-experience/ProductComposerAccessory";
import type { RelationshipProjection } from "./product-experience/experienceTypes";

function validMatrixRoomId(value: string): boolean {
return /^[!#][^:\s]+:[^\s]+$/u.test(value);
}

function validMatrixPermalink(value: string): boolean {
return /^https:\/\/matrix\.to\/#\/[!#][^?\s]+/u.test(value);
}

class YanceElementModule implements Module {
public static readonly moduleApiVersion = "^1.0.0";

public constructor(private readonly api: Api) {}

public async load(): Promise<void> {
this.api.customComponents.registerGlobalRightPanel(() => <YanceWorkspace />);
const navigateSearchResult = async (relationship: RelationshipProjection): Promise<boolean> => {
const permalink = relationship.matrixPermalink?.trim() || "";
if (permalink && validMatrixPermalink(permalink)) {
this.api.navigation.toMatrixToLink(permalink);
return true;
}

const roomId = relationship.matrixRoomId?.trim() || "";
if (roomId && validMatrixRoomId(roomId)) {
this.api.navigation.openRoom(roomId);
return true;
}

return false;
};

this.api.customComponents.registerGlobalRightPanel(
() => <YanceWorkspace navigateSearchResult={navigateSearchResult} />,
);
this.api.customComponents.registerComposerPreview(
(_composerText, roomId) => Boolean(roomId),
(props, originalComponent) => (
Expand Down
Loading
Loading