mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-06 08:16:03 +00:00
feat: add project selection and centralize API calls in browser extension
This commit is contained in:
parent
b46bebdc7f
commit
b52c3bbd74
8 changed files with 522 additions and 99 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { TwitterImporter, type TwitterImportConfig } from '../utils/twitter-import';
|
||||
import { captureTwitterTokens } from '../utils/twitter-auth';
|
||||
import { CONTEXT_MENU_IDS, MESSAGE_TYPES, API_ENDPOINTS, CONTAINER_TAGS } from '../utils/constants';
|
||||
import { CONTEXT_MENU_IDS, MESSAGE_TYPES, CONTAINER_TAGS } from '../utils/constants';
|
||||
import type { ExtensionMessage, MemoryPayload } from '../utils/types';
|
||||
import { getDefaultProject, saveMemory, searchMemories } from '../utils/api';
|
||||
|
||||
export default defineBackground(() => {
|
||||
let twitterImporter: TwitterImporter | null = null;
|
||||
|
|
@ -69,34 +70,23 @@ export default defineBackground(() => {
|
|||
*/
|
||||
const saveMemoryToSupermemory = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => {
|
||||
try {
|
||||
const result = await browser.storage.local.get(['bearerToken']);
|
||||
const bearerToken = result.bearerToken;
|
||||
|
||||
if (!bearerToken) {
|
||||
return { success: false, error: 'No authentication token found' };
|
||||
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT;
|
||||
try {
|
||||
const defaultProject = await getDefaultProject();
|
||||
if (defaultProject?.containerTag) {
|
||||
containerTag = defaultProject.containerTag;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get default project, using fallback:', error);
|
||||
}
|
||||
|
||||
const payload: MemoryPayload = {
|
||||
containerTags: [CONTAINER_TAGS.DEFAULT_PROJECT],
|
||||
containerTags: [containerTag],
|
||||
content: data.highlightedText + '\n\n' + data.html + '\n\n' + data?.url,
|
||||
metadata: { sm_source: 'consumer' },
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}/v3/memories`, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `API call failed: ${response.status}` };
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const responseData = await saveMemory(payload);
|
||||
return { success: true, data: responseData };
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
@ -108,35 +98,11 @@ export default defineBackground(() => {
|
|||
|
||||
const getRelatedMemories = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => {
|
||||
try {
|
||||
const result = await browser.storage.local.get(['bearerToken']);
|
||||
const bearerToken = result.bearerToken;
|
||||
|
||||
if (!bearerToken) {
|
||||
return { success: false, error: 'No authentication token found' };
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}/v3/search`, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
q: data
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `API call failed: ${response.status}` };
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const responseData = await searchMemories(data);
|
||||
const content = responseData.results[0].chunks[0].content;
|
||||
console.log('Content:', content);
|
||||
return { success: true, data: content };
|
||||
}
|
||||
catch(error){
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
.popup-container {
|
||||
width: 320px;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -78,7 +79,8 @@
|
|||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.authenticated, .unauthenticated {
|
||||
.authenticated,
|
||||
.unauthenticated {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
|
@ -114,3 +116,170 @@
|
|||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Project Selection Styles */
|
||||
.project-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 3%;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.project-change-btn {
|
||||
padding: 4px 8px;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.project-change-btn:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.project-current {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-count {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.project-none {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Project Selector Modal */
|
||||
.project-selector {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.project-selector-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project-close-btn:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.project-loading {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.project-list {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.project-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.project-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.project-item.selected {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
|
||||
.project-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.project-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.project-item-count {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.project-item-check {
|
||||
color: #1976d2;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import './App.css';
|
||||
import { getProjects, getDefaultProject, setDefaultProject } from '../../utils/api';
|
||||
import { Project } from '../../utils/types';
|
||||
|
||||
function App() {
|
||||
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [defaultProject, setDefaultProjectState] = useState<Project | null>(null);
|
||||
const [loadingProjects, setLoadingProjects] = useState<boolean>(false);
|
||||
const [showProjectSelector, setShowProjectSelector] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
const result = await chrome.storage.local.get(['bearerToken']);
|
||||
setUserSignedIn(!!result.bearerToken);
|
||||
const isSignedIn = !!result.bearerToken;
|
||||
setUserSignedIn(isSignedIn);
|
||||
|
||||
if (isSignedIn) {
|
||||
try {
|
||||
const defaultProj = await getDefaultProject();
|
||||
setDefaultProjectState(defaultProj);
|
||||
} catch (error) {
|
||||
console.error('Error loading default project:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth status:', error);
|
||||
setUserSignedIn(false);
|
||||
|
|
@ -30,6 +46,44 @@ function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true);
|
||||
try {
|
||||
const projectsList = await getProjects();
|
||||
setProjects(projectsList);
|
||||
console.log('Projects:', projectsList);
|
||||
console.log('Default project:', defaultProject);
|
||||
// If no default project is set and projects are available, set first as default
|
||||
if (!defaultProject && projectsList.length > 0) {
|
||||
const firstProject = projectsList[0];
|
||||
await setDefaultProject(firstProject);
|
||||
setDefaultProjectState(firstProject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading projects:', error);
|
||||
} finally {
|
||||
setLoadingProjects(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectSelect = async (project: Project) => {
|
||||
try {
|
||||
await setDefaultProject(project);
|
||||
setDefaultProjectState(project);
|
||||
setShowProjectSelector(false);
|
||||
} catch (error) {
|
||||
console.error('Error setting default project:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowProjectSelector = () => {
|
||||
console.log('handleShowProjectSelector, projects.length:', projects.length);
|
||||
if (projects.length === 0) {
|
||||
loadProjects();
|
||||
}
|
||||
setShowProjectSelector(true);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="popup-container">
|
||||
|
|
@ -53,10 +107,63 @@ function App() {
|
|||
<div className="content">
|
||||
{userSignedIn ? (
|
||||
<div className="authenticated">
|
||||
<div className="status">
|
||||
<span className="status-indicator signed-in"></span>
|
||||
<span>Signed in</span>
|
||||
|
||||
<div className="project-section">
|
||||
<div className="project-header">
|
||||
<span className="project-label">Default Project:</span>
|
||||
<button
|
||||
className="project-change-btn"
|
||||
onClick={handleShowProjectSelector}
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
<div className="project-current">
|
||||
{defaultProject ? (
|
||||
<div className="project-info">
|
||||
<span className="project-name">{defaultProject.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="project-none">No project selected</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showProjectSelector && (
|
||||
<div className="project-selector">
|
||||
<div className="project-selector-header">
|
||||
<span>Select Default Project</span>
|
||||
<button
|
||||
className="project-close-btn"
|
||||
onClick={() => setShowProjectSelector(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{loadingProjects ? (
|
||||
<div className="project-loading">Loading projects...</div>
|
||||
) : (
|
||||
<div className="project-list">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className={`project-item ${defaultProject?.id === project.id ? 'selected' : ''}`}
|
||||
onClick={() => handleProjectSelect(project)}
|
||||
>
|
||||
<div className="project-item-info">
|
||||
<span className="project-item-name">{project.name}</span>
|
||||
<span className="project-item-count">{project.documentCount} docs</span>
|
||||
</div>
|
||||
{defaultProject?.id === project.id && (
|
||||
<span className="project-item-check">✓</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
|
|||
188
apps/browser-extension/utils/api.ts
Normal file
188
apps/browser-extension/utils/api.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* API service for Supermemory browser extension
|
||||
*/
|
||||
import { API_ENDPOINTS, STORAGE_KEYS } from './constants';
|
||||
import {
|
||||
Project,
|
||||
ProjectsResponse,
|
||||
MemoryPayload,
|
||||
SupermemoryAPIError,
|
||||
AuthenticationError
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Get bearer token from storage
|
||||
*/
|
||||
async function getBearerToken(): Promise<string> {
|
||||
const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]);
|
||||
const token = result[STORAGE_KEYS.BEARER_TOKEN];
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError('Bearer token not found');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated API request
|
||||
*/
|
||||
async function makeAuthenticatedRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = await getBearerToken();
|
||||
|
||||
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, {
|
||||
...options,
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new AuthenticationError('Invalid or expired token');
|
||||
}
|
||||
throw new SupermemoryAPIError(
|
||||
`API request failed: ${response.statusText}`,
|
||||
response.status
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all projects from API
|
||||
*/
|
||||
export async function fetchProjects(): Promise<Project[]> {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest<ProjectsResponse>('/v3/projects');
|
||||
return response.projects;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch projects:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projects from cache or fetch fresh
|
||||
*/
|
||||
export async function getProjects(useCache: boolean = true): Promise<Project[]> {
|
||||
if (useCache) {
|
||||
try {
|
||||
const cached = await chrome.storage.local.get([STORAGE_KEYS.PROJECTS_CACHE]);
|
||||
const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE];
|
||||
|
||||
if (cachedData && cachedData.timestamp && cachedData.projects) {
|
||||
// Cache for 5 minutes
|
||||
const cacheAge = Date.now() - cachedData.timestamp;
|
||||
if (cacheAge < 5 * 60 * 1000) {
|
||||
return cachedData.projects;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to read projects cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const projects = await fetchProjects();
|
||||
|
||||
// Cache the results
|
||||
try {
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.PROJECTS_CACHE]: {
|
||||
projects,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to cache projects:', error);
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default project from storage
|
||||
*/
|
||||
export async function getDefaultProject(): Promise<Project | null> {
|
||||
try {
|
||||
const result = await chrome.storage.local.get([STORAGE_KEYS.DEFAULT_PROJECT]);
|
||||
return result[STORAGE_KEYS.DEFAULT_PROJECT] || null;
|
||||
} catch (error) {
|
||||
console.error('Failed to get default project:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default project in storage
|
||||
*/
|
||||
export async function setDefaultProject(project: Project): Promise<void> {
|
||||
try {
|
||||
await chrome.storage.local.set({
|
||||
[STORAGE_KEYS.DEFAULT_PROJECT]: project,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to set default project:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save memory to Supermemory API
|
||||
*/
|
||||
export async function saveMemory(payload: MemoryPayload): Promise<any> {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest<any>('/v3/memories', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to save memory:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memories using Supermemory API
|
||||
*/
|
||||
export async function searchMemories(query: string): Promise<any> {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest<any>('/v3/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ q: query }),
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to search memories:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tweet to Supermemory API (specific for Twitter imports)
|
||||
*/
|
||||
export async function saveTweet(content: string, metadata: any, containerTag: string = 'sm_project_twitter_bookmarks'): Promise<void> {
|
||||
try {
|
||||
const payload: MemoryPayload = {
|
||||
containerTags: [containerTag],
|
||||
content,
|
||||
metadata,
|
||||
};
|
||||
await saveMemory(payload);
|
||||
} catch (error) {
|
||||
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
|
||||
// Skip if already exists (409 Conflict)
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ export const STORAGE_KEYS = {
|
|||
TWITTER_COOKIE: 'cookie',
|
||||
TWITTER_CSRF: 'csrf',
|
||||
TWITTER_AUTH_TOKEN: 'auth',
|
||||
DEFAULT_PROJECT: 'defaultProject',
|
||||
PROJECTS_CACHE: 'projectsCache',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
type Tweet
|
||||
} from './twitter-utils';
|
||||
import { getTwitterTokens, createTwitterAPIHeaders, type TwitterAuthTokens } from './twitter-auth';
|
||||
import { saveTweet } from './api';
|
||||
|
||||
export type ImportProgressCallback = (message: string) => Promise<void>;
|
||||
|
||||
|
|
@ -53,48 +54,20 @@ class RateLimiter {
|
|||
* @returns Promise that resolves when tweet is imported
|
||||
*/
|
||||
async function importTweet(tweetMd: string, tweet: Tweet): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
browser.storage.local.get(['bearerToken'], ({ bearerToken }) => {
|
||||
if (!bearerToken) {
|
||||
reject(new Error('No bearer token found'));
|
||||
return;
|
||||
}
|
||||
|
||||
const backendURL = 'https://api.supermemory.ai';
|
||||
|
||||
fetch(`${backendURL}/v3/memories`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
containerTags: ['sm_project_twitter_bookmarks'],
|
||||
content: tweetMd,
|
||||
metadata: {
|
||||
sm_source: 'twitter_bookmarks',
|
||||
tweet_id: tweet.id_str,
|
||||
author: tweet.user.screen_name,
|
||||
created_at: tweet.created_at,
|
||||
likes: tweet.favorite_count,
|
||||
retweets: tweet.retweet_count || 0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 409) {
|
||||
resolve(); // Skip if already exists
|
||||
} else {
|
||||
reject(new Error(`Failed to save tweet: ${response.status}`));
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
const metadata = {
|
||||
sm_source: 'consumer',
|
||||
tweet_id: tweet.id_str,
|
||||
author: tweet.user.screen_name,
|
||||
created_at: tweet.created_at,
|
||||
likes: tweet.favorite_count,
|
||||
retweets: tweet.retweet_count || 0,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveTweet(tweetMd, metadata);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save tweet: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ export interface StorageData {
|
|||
cookie?: string;
|
||||
csrf?: string;
|
||||
auth?: string;
|
||||
defaultProject?: Project;
|
||||
projectsCache?: {
|
||||
projects: Project[];
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -128,4 +133,17 @@ export class AuthenticationError extends ExtensionError {
|
|||
super(message, 'AUTH_ERROR');
|
||||
this.name = 'AuthenticationError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
containerTag: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
documentCount: number;
|
||||
}
|
||||
|
||||
export interface ProjectsResponse {
|
||||
projects: Project[];
|
||||
}
|
||||
|
|
@ -114,8 +114,8 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
|
|||
button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON;
|
||||
button.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2147483646;
|
||||
background: #ffffff;
|
||||
color: black;
|
||||
|
|
@ -195,7 +195,7 @@ export function createTwitterImportUI(
|
|||
${isAuthenticated ? `
|
||||
<div>
|
||||
<p style="color: #536471; font-size: 14px; margin: 0 0 12px 0; line-height: 1.4;">
|
||||
This will import all your Twitter bookmarks to Supermemory. Make sure you're logged into Twitter/X first.
|
||||
This will import all your Twitter bookmarks to Supermemory
|
||||
</p>
|
||||
|
||||
<button id="${ELEMENT_IDS.TWITTER_IMPORT_BTN}" style="width: 100%; background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 12px 16px; cursor: pointer; font-size: 14px; font-weight: 500; margin-bottom: 12px;">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue