Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/browser/components/firefoxview/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 11 kB image not shown  

Quelle  ChatsController.sys.mjs   Sprache: unbekannt

 
Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

import { getLogger } from "chrome://browser/content/firefoxview/helpers.mjs";

ChromeUtils.defineESModuleGetters(lazy, {
  AIWindow:
    "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
});

const CHAT_MAP_L10N_IDS = {
  "chat-date-today": "firefoxview-chat-date-today",
  "chat-date-yesterday": "firefoxview-chat-date-yesterday",
  "chat-date-this-month": "firefoxview-chat-date-this-month",
  "chat-date-prev-month": "firefoxview-chat-date-prev-month",
};

const DAYS_IN_MILLIS = 86400000;
const SEARCH_RESULTS_LIMIT = 300;

/**
 * A list of chat items displayed on a card.
 *
 * @typedef {object} CardEntry
 * @property {Array} items - Chat items for this card
 * @property {string} l10nId - Localization ID for the card header
 */

/**
 * Manages chat history display and organization in Firefox View.
 * Handles fetching, searching, grouping by date, and normalizing chat data for display.
 */
export class ChatsController {
  /**
   * @type {{ entries: CardEntry[]; searchQuery: string; }}
   */
  cache;

  /** @type {object} The host element this controller is attached to */
  host;

  /** @type {string} Current search query string */
  searchQuery;

  /** @type {Date} Today's date at midnight */
  #todaysDate;

  /** @type {Date} Yesterday's date at midnight */
  #yesterdaysDate;

  /**
   * Creates a ChatsController instance.
   *
   * @param {object} host - The host element that this controller manages
   * @param {object} options - Configuration options
   * @param {number} [options.searchResultsLimit=300] - Maximum number of search results to return
   */
  constructor(host, options) {
    this.chatStore = lazy.AIWindow.chatStore;
    this.searchQuery = "";
    this.searchResultsLimit =
      options?.searchResultsLimit || SEARCH_RESULTS_LIMIT;
    this.cache = {
      entries: null,
      searchQuery: null,
    };
    this.host = host;

    host.addController(this);
  }

  async deleteChat() {
    const convID = this.host.triggerNode.closedId;
    if (convID) {
      await this.chatStore.deleteConversationById(convID);
      await this.updateCache();
    }
  }

  /**
   * Handles search query events and updates the chat cache.
   *
   * @param {CustomEvent} e - The search query event
   * @param {object} e.detail - Event details
   * @param {string} e.detail.query - The search query string
   */
  onSearchQuery(e) {
    this.searchQuery = e.detail.query;
    this.updateCache();
  }

  /**
   * Gets all chat entries from the cache.
   *
   * @returns {CardEntry[]} Array of card entries containing chat items
   */
  get totalChats() {
    return this.cache.entries || [];
  }

  /**
   * Gets the current search results.
   *
   * @returns {Array} Array of search result items, or empty array if no search is active
   */
  get searchResults() {
    if (this.cache.searchQuery && this.cache.entries?.length) {
      return this.cache.entries[0].items;
    }
    return [];
  }

  /**
   * Gets the total number of chat items across all entries.
   *
   * @returns {number} Total count of chat items
   */
  get totalVisitsCount() {
    return this.totalChats.reduce(
      (count, entry) => count + entry.items.length,
      0
    );
  }

  /**
   * Checks if there are no chats to display.
   *
   * @returns {boolean} True if there are no chat entries
   */
  get isChatEmpty() {
    return !this.totalChats.length;
  }

  /**
   * Updates the cached chat data based on current search query.
   * Fetches either search results or all chats grouped by date.
   * Normalizes chat items and triggers host UI update.
   */
  async updateCache() {
    // Captured before the await to detect mid-flight query changes below.
    const searchQuery = this.searchQuery;
    const entries = searchQuery
      ? await this.#getChatsForSearchQuery(searchQuery)
      : await this.#getAllChatsGroupedByDate();

    if (this.searchQuery !== searchQuery || !entries) {
      return;
    }

    // Normalize chat items for display
    for (const { items } of entries) {
      for (const item of items) {
        this.#normalizeChat(item);
      }
    }

    this.cache = { entries, searchQuery };
    this.host.requestUpdate();
  }

  /**
   * Normalize chat data for fxview-tabs-list.
   *
   * @param {ChatHistoryResult} chat
   *   The chat to format.
   */
  #normalizeChat(chat) {
    // Get most recent page URL (skip if it's the AI Window URL)
    const mostRecentPage = chat.urls?.[chat.urls.length - 1];
    const pageUrl =
      mostRecentPage?.href === lazy.AIWindow.newTabURL
        ? null
        : mostRecentPage?.href;

    // All chats link to AI Window - click handler will decide what to load
    const targetURI = lazy.AIWindow.newTabURL;

    if (pageUrl) {
      chat.icon = pageUrl;
      chat.pageUrl = pageUrl;
      chat.primaryL10nId = "fxviewtabrow-tabs-list-tab";
      chat.primaryL10nArgs = JSON.stringify({
        targetURI,
      });
    }
    chat.url = targetURI;
    chat.time = chat.updatedDate;
    chat.closedId = chat.convId;
    chat.secondaryL10nId = "fxviewtabrow-options-menu-button";
    chat.secondaryL10nArgs = JSON.stringify({
      tabTitle: chat.title || "Untitled Chat",
    });
  }

  /**
   * Fetches and formats chats matching a search query.
   *
   * @param {string} searchQuery - The search query string
   * @returns {Promise<CardEntry[]>} Array with single entry containing search results
   */
  async #getChatsForSearchQuery(searchQuery) {
    try {
      const conversations = await this.chatStore.search(searchQuery, false);
      // Add aliases to conversation objects for display compatibility
      conversations.forEach(conv => {
        conv.convId = conv.id;
        conv.urls = conv.pageMeta?.urls || [];
      });

      return [{ items: conversations }];
    } catch (e) {
      getLogger("ChatsController").error(
        "Search failed for query:",
        searchQuery,
        e.message,
        e.stack
      );
      return [{ items: [] }];
    }
  }

  /**
   * Fetches all chats and groups them by date categories.
   *
   * @returns {Promise<CardEntry[]>} Array of card entries grouped by date
   */
  async #getAllChatsGroupedByDate() {
    const chats = await this.#fetchChats();
    if (!chats || !chats.length) {
      return [];
    }

    this.#setTodaysDate();
    return this.#getChatsForDate(chats);
  }

  /**
   * Groups chats into date-based categories: today, yesterday, this month, previous months.
   *
   * @param {Array} chats - Array of chat objects to group
   * @returns {CardEntry[]} Array of card entries organized by date categories
   */
  #getChatsForDate(chats) {
    const entries = [];
    const chatsFromToday = this.#getChatsFromToday(chats);
    const chatsFromYesterday = this.#getChatsFromYesterday(chats);
    const chatsByDay = this.#getChatsByDay(chats);
    const chatsByMonth = this.#getChatsByMonth(chats);

    if (chatsFromToday.length) {
      entries.push({
        l10nId: CHAT_MAP_L10N_IDS["chat-date-today"],
        items: chatsFromToday,
      });
    }

    if (chatsFromYesterday.length) {
      entries.push({
        l10nId: CHAT_MAP_L10N_IDS["chat-date-yesterday"],
        items: chatsFromYesterday,
      });
    }

    chatsByDay.forEach(dayChats => {
      entries.push({
        l10nId: CHAT_MAP_L10N_IDS["chat-date-this-month"],
        items: dayChats,
      });
    });

    chatsByMonth.forEach(monthChats => {
      entries.push({
        l10nId: CHAT_MAP_L10N_IDS["chat-date-prev-month"],
        items: monthChats,
      });
    });

    return entries;
  }

  /**
   * Filters chats that were updated today.
   *
   * @param {Array} chats - Array of chat objects
   * @returns {Array} Chats from today
   */
  #getChatsFromToday(chats) {
    const todayStart = this.#todaysDate.getTime();
    const todayEnd = todayStart + DAYS_IN_MILLIS;
    return chats.filter(
      chat => chat.updatedDate >= todayStart && chat.updatedDate < todayEnd
    );
  }

  /**
   * Filters chats that were updated yesterday.
   *
   * @param {Array} chats - Array of chat objects
   * @returns {Array} Chats from yesterday
   */
  #getChatsFromYesterday(chats) {
    const todayStart = this.#todaysDate.getTime();
    const yesterdayStart = this.#yesterdaysDate.getTime();
    return chats.filter(
      chat =>
        chat.updatedDate >= yesterdayStart && chat.updatedDate < todayStart
    );
  }

  /**
   * Groups chats by individual days within the current month.
   * Excludes today and yesterday chats.
   *
   * @param {Array} chats - Array of chat objects
   * @returns {Array<Array>} Array of arrays, each containing chats from the same day
   */
  #getChatsByDay(chats) {
    const yesterdayStart = this.#yesterdaysDate.getTime();
    const chatsPerDay = [];
    let currentDay = null;
    let currentDayChats = [];

    for (const chat of chats) {
      if (chat.updatedDate >= yesterdayStart) {
        continue;
      }

      const chatDate = new Date(chat.updatedDate);
      if (!this.#isSameMonth(chatDate, this.#todaysDate)) {
        break;
      }

      const dayKey = `${chatDate.getFullYear()}-${chatDate.getMonth()}-${chatDate.getDate()}`;
      if (dayKey !== currentDay) {
        if (currentDayChats.length) {
          chatsPerDay.push(currentDayChats);
        }
        currentDay = dayKey;
        currentDayChats = [chat];
      } else {
        currentDayChats.push(chat);
      }
    }

    if (currentDayChats.length) {
      chatsPerDay.push(currentDayChats);
    }

    return chatsPerDay;
  }

  /**
   * Groups chats by month for dates outside the current month.
   * Excludes today, yesterday, and current month chats.
   *
   * @param {Array} chats - Array of chat objects
   * @returns {Array<Array>} Array of arrays, each containing chats from the same month
   */
  #getChatsByMonth(chats) {
    const yesterdayStart = this.#yesterdaysDate.getTime();
    const chatsPerMonth = [];
    let currentMonth = null;
    let currentMonthChats = [];

    for (const chat of chats) {
      if (chat.updatedDate >= yesterdayStart) {
        continue;
      }

      const chatDate = new Date(chat.updatedDate);
      if (this.#isSameMonth(chatDate, this.#todaysDate)) {
        continue;
      }

      const monthKey = `${chatDate.getFullYear()}-${chatDate.getMonth()}`;
      if (monthKey !== currentMonth) {
        if (currentMonthChats.length) {
          chatsPerMonth.push(currentMonthChats);
        }
        currentMonth = monthKey;
        currentMonthChats = [chat];
      } else {
        currentMonthChats.push(chat);
      }
    }

    if (currentMonthChats.length) {
      chatsPerMonth.push(currentMonthChats);
    }

    return chatsPerMonth;
  }

  /**
   * Check if two dates have the same month and year.
   *
   * @param {Date} dateToCheck
   * @param {Date} month
   * @returns {boolean}
   */
  #isSameMonth(dateToCheck, month) {
    return (
      dateToCheck.getMonth() === month.getMonth() &&
      dateToCheck.getFullYear() === month.getFullYear()
    );
  }

  /**
   * Sets today's and yesterday's dates at midnight for date comparison operations.
   */
  #setTodaysDate() {
    const now = new Date();
    this.#todaysDate = new Date(
      now.getFullYear(),
      now.getMonth(),
      now.getDate()
    );
    this.#yesterdaysDate = new Date(
      now.getFullYear(),
      now.getMonth(),
      now.getDate() - 1
    );
  }

  /**
   * Fetches chats from the chat store.
   *
   * @returns {Promise<Array>} Array of chat objects, or empty array on error
   */
  async #fetchChats() {
    try {
      // Use chatHistoryView for paginated, optimized results
      // Get first 100 chats, sorted by date descending
      const chats = await this.chatStore.chatHistoryView(1100, "desc");
      return chats;
    } catch (e) {
      getLogger("ChatsController").error("Error fetching chats", e);
      return [];
    }
  }
}

[Dauer der Verarbeitung: 0.23 Sekunden, vorverarbeitet 2026-08-26]